Hampir setiap aplikasi PHP yang berhubungan dengan dunia luar menyimpan kredensial: password database, API key payment gateway, kunci penandatanganan webhook, hingga token layanan email. Nilai-nilai ini berbeda dari konfigurasi biasa — sekali bocor, penyerang dapat masuk ke sistem tanpa perlu mengeksploitasi celah apa pun di kode Anda. Chapter ini membahas siklus hidup secrets secara utuh: di mana menyimpannya, bagaimana memutarnya, cara membatasi jangkauannya, bagaimana mendeteksi kebocoran, dan apa yang harus dilakukan saat kebocoran benar-benar terjadi.

Sumber Penyimpanan Secret #

Ada tiga tempat umum penyimpanan kredensial, masing-masing dengan perannya sendiri:

SumberCocok untukCatatan
Environment variable (systemd, container runtime)Server dan container produksiSederhana dan teruji; nilai dapat terbaca lewat /proc/<pid>/environ oleh pengguna yang sama di mesin itu
Berkas .envDevelopment sajaPraktis, tetapi menjadi sumber kebocoran nomor satu saat tak sengaja ter-commit ke git
Secret manager (HashiCorp Vault, AWS Secrets Manager, GCP Secret Manager)Produksi berskala timMenyediakan enkripsi at-rest, audit log akses, rotasi terjadwal, dan kontrol akses per aplikasi

Environment variables yang kita pelajari pada chapter Environment Variables tetap menjadi jalur masuk utama ke aplikasi: pada pola secret manager, aplikasi menarik kredensial saat boot (atau membacanya lewat extension/pustaka khusus) lalu menyimpannya di environment prosesnya sendiri. Perbedaannya, secret manager yang menjadi sumber kebenaran — satu tempat untuk mencabut, memutar, dan mengaudit, bukan berkas yang tersebar di puluhan server. Untuk aplikasi kecil dengan satu server, environment yang diset systemd sudah memadai; seiring bertambahnya jumlah layanan, manfaat audit log dan rotasi otomatis dari Vault atau layanan cloud semakin terasa.

Rotasi dan Masa Berlaku Secret #

Setiap secret sebaiknya diperlakukan seperti susu: punya tanggal kedaluwarsa. Rotasi berkala memperkecil jendela waktu penyerang setelah kebocoran yang tidak terdeteksi — token yang dicuri enam bulan lalu tidak bernilai apa-apa jika sudah tidak berlaku. Kebijakan umum di industri: kredensial sensitif tinggi (kunci penandatanganan pembayaran) diputar setiap 30-90 hari, sedangkan kredensial berisiko lebih rendah boleh 90-180 hari. Rotasi paling aman dijalankan otomatis oleh sistem yang mengeluarkan kredensial, bukan oleh manusia yang menyalin-tempel nilai baru.

Umur sebuah kredensial mudah diperiksa secara programatik jika tanggal rotasi terakhirnya tercatat:

php
<?php
function perluRotasi(DateTimeImmutable $terakhirRotasi, DateTimeImmutable $sekarang, int $maksHari = 90): bool
{
    return $terakhirRotasi->diff($sekarang)->days >= $maksHari;
}

$terakhirRotasi = new DateTimeImmutable('2025-11-01', new DateTimeZone('Asia/Jakarta'));
$sekarang = new DateTimeImmutable('2026-02-15', new DateTimeZone('Asia/Jakarta'));

var_dump(perluRotasi($terakhirRotasi, $sekarang));
var_dump(perluRotasi($terakhirRotasi, $sekarang, 180));

Output yang Dihasilkan:

text
bool(true)
bool(false)

Mari kita bedah contoh di atas:

  1. Baris 2-5 (function perluRotasi): menghitung selisih hari antara rotasi terakhir dan tanggal acuan memakai diff()->days, lalu membandingkannya dengan batas kebijakan. Parameter $maksHari diberi nilai bawaan 90.
  2. Baris 7-8: kedua tanggal dibekukan sebagai DateTimeImmutable eksplisit — pada skrip nyata, tanggal rotasi biasanya dibaca dari metadata secret manager, bukan ditulis langsung.
  3. Baris 10-11: jarak 2025-11-01 ke 2026-02-15 adalah 106 hari — melampaui kebijakan 90 hari (true), tetapi masih di bawah kebijakan yang lebih longgar 180 hari (false).

Scoping Kredensial per Layanan #

Prinsip least privilege berlaku juga pada kredensial: setiap layanan sebaiknya memegang kredensialnya sendiri yang hanya bisa mengakses apa yang memang dibutuhkannya. Aplikasi toko tidak perlu satu password database bersama dengan layanan laporan — cukup buat dua akun: toko_app dengan hak SELECT/INSERT/UPDATE pada database transaksi, dan laporan_ro dengan hak baca saja pada replika. Dengan begitu, kebocoran pada satu layanan tidak otomatis menjadi kebocoran total.

Pemetaan yang sama berlaku untuk layanan eksternal. Modul pembayaran sebaiknya memegang API key yang hanya bisa menagih dan mengecek status — bukan key dengan hak penuh akun merchant yang bisa mengubah rekening pencairan. Hampir semua penyedia layanan modern menyediakan variasi key dengan scope terbatas; luangkan waktu untuk memilih scope paling kecil yang masih membuat aplikasi berfungsi. Selalu dokumentasikan pemetaan layanan → kredensial agar saat insiden Anda tahu persis apa yang harus dicabut.

Memindai Kebocoran dengan Scanner Sederhana #

Kebocoran paling sering terjadi bukan karena serangan, melainkan karena kecelakaan: berkas konfigurasi lama ter-commit, error handler mencetak koneksi database, atau log menangkap header Authorization. Alat seperti gitleaks dan trufflehog memindai repositori dan log secara menyeluruh; untuk memahami cara kerjanya, kita bisa menulis scanner versi mini dengan regex PHP:

php
<?php
function pindaiRahasia(string $konten): array
{
    $pola = [
        'AWS access key' => '/AKIA[0-9A-Z]{16}/',
        'Private key PEM' => '/-----BEGIN [A-Z ]*PRIVATE KEY-----/',
        'Password hardcode' => '/(?i)(password|passwd|pwd)\s*=\s*\S{8,}/',
        'API key atau secret' => '/(?i)(api[_-]?key|secret|token)\s*=\s*\S{8,}/',
    ];
    $temuan = [];
    foreach ($pola as $jenis => $regex) {
        if (preg_match($regex, $konten) === 1) {
            $temuan[] = $jenis;
        }
    }
    return $temuan;
}

$berkasDicurigai = <<<'KODE'
<?php
// konfigurasi lama yang tak sengaja ter-commit
$dbPassword = "sandiRahasiaProd123";
$paymentApiKey = "sk_live_abcdef1234567890";
$awsAccessKey = "AKIAIOSFODNN7EXAMPLE";
KODE;

$temuan = pindaiRahasia($berkasDicurigai);
echo count($temuan) . " temuan:\n";
foreach ($temuan as $i => $jenis) {
    printf("%d. %s\n", $i + 1, $jenis);
}

Output yang Dihasilkan:

text
3 temuan:
1. AWS access key
2. Password hardcode
3. API key atau secret

Mari kita telaah cara kerja scanner ini:

  1. Baris 4-9 (array $pola): empat pola regex — prefiks AWS access key (AKIA diikuti 16 karakter alfanumerik), blok private key PEM, penugasan password = ... dengan nilai minimal 8 karakter, dan penugasan api_key/secret/token. Modifier (?i) membuat pencocokan tidak peduli kapitalisasi sehingga dbPassword pun tertangkap.
  2. Baris 19-25 (heredoc $berkasDicurigai): contoh berkas yang mengandung tiga kredensial palsu — nilainya fiktif, tetapi polanya persis seperti kebocoran nyata.
  3. Baris 27-31: hasil pemindaian hanya mencatat jenis temuan, bukan nilainya — output scanner pun tidak boleh menjadi sumber kebocoran baru.
  4. Hasil eksekusi: tiga dari empat pola cocok (pola private key PEM tidak ada isinya), terurut mengikuti urutan definisi pola.

Menyamarkan Secret di Log #

Log aplikasi adalah tempat kebocoran yang sering luput: baris Connection refused for user 'budi' masih aman, tetapi DSN lengkap atau header request yang dicetak mentah bisa membocorkan kredensial. Aturan praktisnya: catat peristiwa (siapa, kapan, aksi apa) tanpa pernah mencatat nilai secret. Bila Anda benar-benar perlu menunjukkan bahwa sebuah nilai dipakai, tampilkan versi tersamarkan:

php
<?php
function maskRahasia(string $nilai): string
{
    if (strlen($nilai) <= 8) {
        return str_repeat('*', 8);
    }
    return substr($nilai, 0, 4) . '...' . substr($nilai, -4);
}

echo maskRahasia('sk_live_abcdef1234567890') . "\n";
echo maskRahasia('rahasia') . "\n";
echo maskRahasia('YOUR_DATABASE_PASSWORD') . "\n";

Output yang Dihasilkan:

text
sk_l...7890
********
YOUR...WORD

Function maskRahasia() menyisakan empat karakter pertama dan empat terakhir agar operator masih bisa mencocokkan nilai di log dengan nilai di sistem (cukup untuk memastikan “key yang mana yang dipakai”), sementara bagian tengahnya disembunyikan. Nilai pendek disamarkan penuh dengan jumlah bintang tetap delapan — panjang samaran yang selalu sama mencegah lawan mengira-ngira panjang kredensial asli dari panjang output log. Di sisi infrastruktur, secret manager menyediakan audit log akses secara otomatis: setiap pembacaan secret tercatat beserta identitas pembacanya, sehingga investigasi insiden tidak bergantung pada kerapian log aplikasi Anda semata.

Runbook Rotasi Darurat #

Ketika kebocoran dikonfirmasi, waktu adalah segalanya — bukan saatnya berpikir dari nol. Siapkan runbook tertulis dan latih sesekali, kira-kira seperti urutan berikut:

  1. Konfirmasi dan batasi: pastikan kebocoran nyata (bukan nilai placeholder), lalu tangkal penyalahgunaan langsung — blokir IP penyerang, cabut sesi/token aktif yang diketahui bocor, atau nonaktifkan sementara integrasi yang terdampak.
  2. Terbitkan kredensial pengganti di sistem penerbitnya (secret manager atau penyedia layanan).
  3. Sebar ke aplikasi melalui mekanisme konfigurasi biasa — restart atau reload layanan secara bergiliran agar tidak ada downtime.
  4. Cabut kredensial lama setelah seluruh instance terbukti memakai yang baru; pantau error autentikasi untuk menangkap instance yang tertinggal.
  5. Periksa jejak akses: audit log secret manager dan log layanan untuk aktivitas asing selama periode kredensial lama masih berlaku.
  6. Cari akar masalah: berkas mana yang bocor, lewat jalur apa (git history, log, laptop hilang), dan perbaiki jalurnya.
  7. Tulis post-mortem tanpa mencari siapa yang salah: perbaiki proses (misalnya wajib pre-commit hook) dan jadwalkan rotasi berikutnya.

Memisahkan Secret Development dan Produksi #

Lingkungan development sebaiknya tidak pernah memegang kredensial produksi — bahkan yang “hanya untuk membaca”. Tiga aturan pemisahan yang praktis: pertama, nilai development selalu dummy atau lokal (budi, sandi_dev_123, database SQLite di laptop) sehingga bocor sekalipun tidak ada yang bisa dirugikan. Kedua, data produksi tidak pernah disalin ke development; gunakan data seed sintetis seperti nama Budi dan Siti. Ketiga, akun dan key development dibuat terpisah di penyedia layanan (bukan key produksi dengan scope sempit) agar kebijakan pemblokiran berbeda bisa diterapkan — key sandbox pembayaran bisa dicabut kapan saja tanpa menyentuh produksi.

Secret di CI/CD #

Pipeline CI adalah konsumen kredensial yang berisiko: ia mengeksekusi kode dari banyak kontributor dan menyimpan riwayat log yang panjang. Platform CI modern (GitHub Actions, GitLab CI) menyediakan masked variables — nilai yang disimpan terenkripsi dan otomatis disamarkan (***) bila tidak sengaja tercetak di log:

yaml
name: deploy
on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Deploy ke produksi
        env:
          DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
        run: ./bin/deploy.sh

Blok YAML di atas menyuntikkan secret DEPLOY_TOKEN sebagai environment variable hanya pada langkah deploy — tidak tersedia di langkah lain dan tidak muncul di log. Perhatikan dua kebiasaan aman di dalamnya: kredensial masuk lewat env per-langkah (bukan di-export global), dan skrip deploy membacanya lewat environment (getenv('DEPLOY_TOKEN') di sisi PHP), bukan menerima nilai sebagai argumen command line yang bisa terbaca di daftar proses. Untuk pipeline di repositori publik, ingat bahwa pull request dari fork tidak memperoleh akses ke secrets — ini disengaja agar kontributor luar tidak bisa mencuri kredensial Anda.

Ringkasan #

Manajemen secret yang sehat berdiri di atas empat pilar: penyimpanan yang tepat (environment untuk server, secret manager untuk skala tim), rotasi berkala dengan masa transisi, scoping minimal per layanan, serta deteksi kebocoran berlapis dari pre-commit sampai CI. Ketika insiden terjadi, runbook yang sudah dilatih mengubah kepanikan menjadi urutan langkah yang terukur. Dengan kredensial kini tersimpan aman di environment proses, kita siap naik ke lapisan berikutnya: bagaimana aplikasi PHP itu sendiri dijalankan sebagai layanan yang stabil melalui PHP-FPM .

Referensi #