.env Terlanjur Masuk Git? Ini Cara Menghapusnya dengan Aman

pindipin
31 August 2026
15 min read
.env Terlanjur Masuk Git? Ini Cara Menghapusnya dengan Aman

Semua orang yang pernah salah commit tahu rasa panik itu: kamu baru saja belajar kalau .env project Laravel-mu — berisi database password, mail credentials, dan API keys — ternyata ikut ter-push ke GitHub. Mungkin waktu push kamu sadar, atau lebih sering lagi kamu baru tahu dari email alert secret scanning dari GitHub, atau dari PR yang menunjukkan file itu tercantum.

Di artikel sebelumnya tentang .gitignore Laravel kita membahas cara mencegah file ini masuk tracking sejak awal. Tapi kalau kamu sudah melewati titik itu, pencegahannya telat. Yang sekarang perlu kamu lakukan adalah membetulkan kesalahannya: mengeluarkan .env dari repository, membersihkan jejaknya dari seluruh riwayat commit, dan memastikan kredensial yang sudah terlanjur bocor tidak bisa lagi dimanfaatkan orang.

Artikel ini bukan sekadar "hapus file lalu selesai". Ada urutan penanganan yang keliru dijalankan oleh banyak orang — dan urutan itu justru yang menentukan apakah datamu benar-benar aman atau tidak.

Kredensial yang Bocor Adalah Kredensial yang Harus Mati

Ini poin yang paling sering terlewat, dan karena itu saya taruh paling depan.

Refleks kebanyakan developer saat sadar .env sudah ter-push adalah langsung menghapus file itu dari Git. Tapi bukan itu langkah pertamanya. Langkah pertama adalah menganggap semua isi .env lama itu sudah bocor, lalu merotasi secret-nya.

Kenapa harus begitu? Karena bot otomatis di internet memindai repository publik dalam hitungan detik setelah sebuah commit muncul. Mereka mencari pola seperti DB_PASSWORD= atau MAIL_PASSWORD= dan langsung mengekstrak nilainya. Menghapus file dari history Git hanya membersihkan repository-mu — tidak menghapus kredensial itu dari tangan siapa pun yang sudah sempat membacanya, dan tidak mengubah kenyataan bahwa secret itu sudah terekspos di internet selama beberapa waktu.

Sementara itu, membersihkan riwayat commit membutuhkan waktu: kamu harus menjalankan tool, melakukan force push, berkoordinasi dengan tim. Selama proses itu berjalan, kredensial lamamu tetap hidup di luar sana. Itu sebabnya rotasi kredensial — ganti database password, rotate semua API keys, ganti SMTP credentials, dan seterusnya — dilakukan lebih dulu, sebelum kamu membersihkan git-nya.

Ada satu hal lagi yang sering disalahpahami: git rm --cached .env saja tidak cukup untuk menghapus data sensitif. Perintah itu hanya menghentikan Git untuk melacak file di commit-commit berikutnya. Isi file di commit-commit lama tetap ada di dalam riwayat Git, dan siapa pun bisa membukanya kapan saja. Ini bukan menghapus, ini hanya berhenti mencatat yang baru.

Jadi bayangkan alurnya begini: rotasi secret dulu (amankan), baru urus git-nya (rapikan). Urutan ini yang bikin kamu tidak menyelesaikan masalah dengan setengah hati.

Langkah 1: Berhenti Melacak .env pada Commit Berikutnya

Setelah secret diganti, giliran membersihkan tracking. Tujuannya dua: mengeluarkan .env dari index Git agar tidak ikut di commit mendatang, dan memastikan file tetap ada di disk mu. git rm --cached melakukan persis itu — menghapus file dari index Git, tapi membiarkan working tree tetap utuh.

# Hapus .env dari index Git, file di disk tetap aman
git rm --cached .env

Lalu pastikan .env masuk ke .gitignore agar tidak ter-jebak lagi:

# Tambahkan .env ke .gitignore (kalau belum ada)
echo ".env" >> .gitignore

Terakhir, commit dan push perubahannya:

git add .gitignore
git commit -m "chore: stop tracking .env and add to gitignore"
git push origin <branch-name>

Catatan penting soal git rm: kalau kamu menjalankan git rm tanpa --cached, file akan terhapus dari disk dan tracking. Itu operasi yang berbeda dan pastinya bukan yang kamu mau di sini.

Setelah langkah ini, .env tidak akan muncul lagi di commit baru. Tapi ingat — ini baru membersihkan permukaannya. Isi .env masih hidup di commit-commit lama, dan ke situlah langkah berikutnya mengarah.

Langkah 2: Menghapus .env dari Seluruh Riwayat Commit

Ini bagian yang sebenarnya menentukan apakah rahasia-mu benar-benar pergi dari repository atau tidak. Ada tiga tool yang biasa disebut untuk pekerjaan ini, dan salah satunya sebaiknya kamu hindari.

Metode A: Menggunakan git-filter-repo (Rekomendasi Resmi)

git-filter-repo adalah tool modern yang direkomendasikan secara resmi oleh dokumentasi Git dan GitHub sebagai pengganti git filter-branch yang sudah deprecated. Ditulis dalam Python, dan tersedia untuk semua platform utama.

Instalasinya tergantung sistem operasi:

# Windows (via pip)
pip install git-filter-repo

# macOS (via Homebrew)
brew install git-filter-repo

Karena tool ini sangat destruktif, kamu harus menjalankannya di clone baru (fresh clone). git filter-repo menolak berjalan di repository yang punya modifikasi lokal atau bukan fresh clone — ini proteksi built-in supaya kamu tidak merusak kerja yang sedang berjalan. Kalau terpaksa, ada flag --force, tapi sebaiknya jangan digunakan kecuali benar-benar tahu apa yang kamu lakukan.

# Buat clone baru dari remote
git clone <URL-REPOSITORI>
cd <nama-direktori>

# Hapus .env dari seluruh riwayat
git filter-repo --invert-paths --path .env

--invert-paths --path .env artinya: balikkan seleksi path sehingga semua path kecuali .env dipertahankan — dengan kata lain, buang .env dari seluruh commit.

Kalau versi git-filter-repo kamu sudah v2.47 atau lebih baru, ada tambahan opsional yang berguna untuk kasus data sensitif seperti ini: flag --sensitive-data-removal. Menambahkannya ke perintah yang sama (git filter-repo --sensitive-data-removal --invert-paths --path .env) membuat tool ikut mengumpulkan informasi seperti commit-commit pertama yang berubah dan LFS orphan yang mungkin terlewat — data yang justru dibutuhkan GitHub Support saat mereka memproses permintaan purge cache-mu nanti. Tanpa flag ini perintah tetap sah dan berfungsi, jadi anggap ini penyempurnaan, bukan keharusan.

Ada satu efek samping yang wajib kamu tahu terkait remote origin. Di mode normal (tanpa --sensitive-data-removal), git filter-repo otomatis menghapus remote origin setelah selesai — ini sengaja, sebagai pengaman agar kamu tidak tak-sengaja melakukan push ke remote yang salah setelah history berubah. Kalau kamu memakai mode normal, kamu harus menambahkannya kembali:

git remote add origin <URL-REPOSITORI>

Sebaliknya, kalau kamu memakai flag --sensitive-data-removal di baris sebelumnya, origin dibiarkan utuh — memang dirancang begitu, karena kamu memang akan force push hasil rewrite kembali ke remote yang sama. Dalam mode itu kamu tidak perlu menambahkannya kembali.

Metode B: Menggunakan BFG Repo-Cleaner (Lebih Cepat)

BFG Repo-Cleaner adalah alternatif yang jauh lebih cepat dari git filter-branch, ditulis dalam Scala/Java. Cocok kalau repository-mu sangat besar, atau kamu tidak punya Python terinstall tapi punya Java. Kamu butuh JRE 11+ dan file .jar-nya, misalnya bfg-1.15.0.jar.

Langkahnya kloning dengan flag --mirror dulu:

git clone --mirror <URL-REPOSITORI>

Lalu jalankan BFG untuk menghapus file .env:

java -jar bfg.jar --delete-files .env <nama-direktori-reposi>.git

Ada satu jebakan yang perlu disadari sejak awal: BFG secara default tidak menyentuh commit terakhir di HEAD. Ini proteksi yang disengaja agar aplikasi yang sedang berjalan tidak tiba-tiba kehilangan file yang direferensikan. Konsekuensinya, kalau .env masih ada di commit terakhir, BFG tidak akan menghapusnya — kecuali kamu sudah menghapusnya di commit terakhir (lewat Langkah 1 di atas) atau memakai flag --no-blob-protection.

Karena itulah urutannya penting: pastikan .env sudah di-ignore dan di-commit (Langkah 1) sebelum menjalankan BFG, atau gunakan --no-blob-protection kalau kamu yakin.

Mengapa Menghindari git filter-branch

git filter-branch mungkin nama yang paling sering muncul di tutorial lama, dan kamu akan melihat peringatan keras langsung saat menjalankannya: tool ini sudah deprecated dan dengan tegas disarankan untuk tidak dipakai. Alasannya berlapis: performanya lambat di repository besar, rawan merusak encoding commit message, tidak menangani tag beranotasi dengan baik, dan mudah bikin duplikasi riwayat kalau salah langkah.

Command referensinya (jangan dijadikan andalan):

git filter-branch --force --index-filter "git rm --cached --ignore-unmatch .env" --prune-empty --tag-name-filter cat -- --all

Untuk project baru, gunakan git-filter-repo. Untuk repository raksasa yang butuh kecepatan ekstra dan berasal dari ekosistem Java, BFG bisa jadi pilihan. Keduanya jauh lebih aman daripada git filter-branch.

Langkah 3: Force-Push ke Server Remote

History lokal sudah bersih. Sekarang kamu perlu memaksa perubahan itu masuk ke server remote — karena commit-commit lama yang berisi .env masih berada di sana.

git push origin --force --mirror

Kalau kamu pakai clone biasa (bukan mirror), dua perintah ini ekuivalen dan sering dipakai:

git push origin --force --all
git push origin --force --tags

Mana yang lebih baik? Untuk pembersihan data sensitif, GitHub Docs sendiri merekomendasikan git push --force --mirror origin. Yang membedakannya: --mirror ikut memaksa semua ref (termasuk yang sudah kosong setelah rewrite) terhapus dari remote, jadi pembersihannya lebih menyeluruh. Cara --force --all --tags tetap berfungsi untuk kasus umum, tapi --mirror adalah pilihan yang lebih lengkap untuk skenario seperti ini. Kalau kamu tadi pakai BFG dengan mirror clone, --mirror memang perintah yang tepat.

Ini momen paling berisiko untuk tim. Force push menulis ulang riwayat commit. Kalau ada developer lain yang masih memegang clone lama, dan mereka menjalankan git pull biasa, Git akan menggabungkan riwayat kotor mereka ke riwayat bersih yang baru — dan file .env kembali masuk. Inilah yang disebut rekontaminasi, dan ini cara paling umum kebocoran "muncul lagi" setelah kamu repot-repot membersihkannya.

Koordinasi sebelum force push bukan pilihan, itu keharusan:

  • Putuskan dengan tim kapan kamu akan force push, idealnya saat tidak ada yang sedang aktif bekerja (freeze commit).
  • Setelah force push, setiap developer harus membuang clone lokal lama mereka dan melakukan clone ulang dari remote. Ini cara teraman — clone baru dijamin bersih, tanpa risiko menggabungkan riwayat lama.
  • Kalau ada developer yang punya kerja lokal yang belum di-push, ada satu langkah yang harus dilakukan sebelum membuang clone: menyimpan kerja itu dulu. Kalau tidak, kerja mereka ikut terbuang bersama clone lama.

Jangan biarkan siapa pun "menyelamatkan" riwayat lama mereka dengan git pull biasa — itu akan menggabungkan riwayat kotor kembali ke clone yang bersih. Salah satu jebakan tentang git reset --hard juga perlu diluruskan di sini: perintah itu tidak menyimpan kerja lokal. Di balik namanya, reset --hard justru membuang perubahan di working tree dan index, dan tidak mempertahankan commit lokal yang belum di-push. Jadi jangan pernah memakai reset --hard sebagai cara "menyelamatkan" pekerjaan.

Untuk developer yang punya kerja lokal sebelum rewrite history di-force push, alurnya yang benar kira-kira begini:

  1. Sebelum tim menulis ulang history, amankan semua kerja lokal kamu. Perubahan yang belum di-commit di working tree dan index tidak ikut tersimpan di git branch — untuk itu, stash dulu (git stash) atau commit dulu sebagai commit darurat. Setelah itu buat branch cadangan dari posisi lokal kamu saat ini: git branch backup/my-branch. Kalau kerjaanmu berupa beberapa commit yang belum di-push, kamu bisa menyimpannya lewat branch ini atau memetiknya (cherry-pick) nanti setelah riwayat baru tersedia.
  2. Setelah rewrite history di-force push ke remote, ambil riwayat baru itu: git fetch origin, lalu arahkan branch kerja kamu ke posisi remote yang sudah bersih dengan git reset --hard origin/<branch>. Karena kamu sudah menyimpan kerja lokal di langkah 1, reset di sini aman — kamu tidak kehilangan apa pun.

Satu catatan jujur yang perlu dipahami seluruh tim: kalau ada data yang pernah di-push ke remote tapi kemudian dipangkas oleh rewrite history, dan seorang developer tetap memegangnya di clone lokal yang masih kotor, maka saat mereka push, data itu bisa "hidup lagi" dan membuat isi .env masuk ulang ke sejarah. Inilah alasan kenapa clone ulang dari nol adalah pilihan paling bersih dan paling mudah dihafalkan untuk konteks keamanan seperti ini — bukan mempertahankan clone lama dengan berbagai trik.

Langkah 4: Pembersihan Fisik Lokal dan Server-Side Cache

Force push membersihkan server, tapi di mesin lokal mu masih ada jejak. Objek-objek lama yang berisi isi .env tadi masih menggantung di folder .git sebagai objek dangling, dan masih bisa di-recover lewat reflog. Bersihkan keduanya:

git reflog expire --expire=now --all
git gc --prune=now --aggressive

git reflog expire --expire=now --all membuat semua entry reflog kedaluwarsa seketika, dan git gc --prune=now --aggressive menghapus objek yang tidak lagi dirujuk. Karena .env tadi tidak dirujuk oleh commit mana pun, isinya terhapus secara fisik dari disk lokal.

Sekarang soal sisi server. Ini bagian yang sering bikin orang mengira pekerjaannya sudah selesai padahal belum.

Bahkan setelah force push, commit lama yang berisi .env masih bisa diakses di GitHub. Misalnya lewat link langsung ke SHA commit tertentu, atau lewat halaman Pull Request yang terasosiasi dengan commit tersebut — GitHub menyimpan semacam cached view dari commit-commit itu. Menghapus commit dari semua cabang dan tag tidak otomatis menghapus cache ini. Satu-satunya cara membersihkannya secara permanen adalah menghubungi GitHub Support dan meminta mereka melakukan purge cache repositori.

Satu hal yang perlu kamu tahu tentang permintaan ini: GitHub Support tidak selalu mengabulkannya. Mereka hanya bersedia membantu menghapus data sensitif kalau mereka menilai risikonya tidak bisa dimitigasi dengan cara lain — dalam hal ini, kalau kredensial yang terlanjur bocor sudah dirotasi. Karena rotasi itu memang sudah kita lakukan di langkah pertama, permintaanmu punya dasar yang kuat. Tapi jangan berharap Support membersihkan cache kalau secret-nya sengaja dibiarkan hidup; itu bukan tugas mereka.

Satu kenyataan yang perlu diterima: kalau repository-mu sudah di-fork oleh orang lain sebelum kamu membersihkan, .env itu tetap ada di fork mereka, dan kamu tidak punya kuasa untuk menghapusnya dari sana. Kalau data itu sangat kritis dan repository-nya publik, opsi terakhir yang masuk akal adalah menghapus repository asli dan membuat yang baru.

Spesifik Laravel: Merotasi APP_KEY tanpa Merusak Data

Bagian ini khusus bagi project Laravel yang menyimpan APP_KEY di dalam .env yang bocor. Merotasi key ini tidak semudah mengganti password database, karena satu key dipakai untuk banyak hal sekaligus.

Kalau kamu menjalankan php artisan key:generate dan langsung mengganti APP_KEY, tiga hal ini ikut terdampak:

  • Session. Sesion cookie dienkripsi memakai APP_KEY. Semua session aktif terputus dan pengguna otomatis log out.
  • Data terenkripsi. Setiap data yang disimpan dengan facade Crypt (misalnya Crypt::encryptString) tidak bisa didekripsi lagi dengan key baru, dan akan melempar DecryptException. Kalau database produksimu berisi kolom terenkripsi, ini artinya data tidak terbaca.
  • Signed URLs. Tautan berbatas waktu seperti email verification link atau link download sementara, yang dibuat sebelum rotasi, menjadi tidak valid karena tanda tangan HMAC-nya berubah.

Mulai Laravel 11 ke atas, ada mekanisme untuk merotasi key secara bertahap tanpa memutus semua data sekaligus: APP_PREVIOUS_KEYS. Idenya, kamu masukkan key lama yang bocor ke variabel ini, lalu biarkan aplikasi mencoba mendekripsi dengan key baru dulu, dan kalau gagal, mencoba key-key lama secara berurutan.

APP_KEY="base64:KeyBaruYangDihasilkanArtisan..."
APP_PREVIOUS_KEYS="base64:KeyLamaYangBocorAtauLama..."

Tapi di sinilah kamu harus berhenti sebentar dan memutuskan satu hal: apakah key lama itu hanya di-rotasi sebagai rutinitas, atau benar-benar sudah bocor ke orang lain? Jawabannya mengubah cara kamu memakai APP_PREVIOUS_KEYS.

  • Rotasi rutin (misalnya pergantian key terjadwal yang tidak ada indikasi kebocoran): menyimpan key lama di APP_PREVIOUS_KEYS sepenuhnya aman. Tidak ada pihak luar yang tahu key itu, jadi tidak ada risiko tambahan.
  • Kebocoran terkonfirmasi (key-nya ikut masuk .env yang ter-push ke repository publik): menyimpan key lama di APP_PREVIOUS_KEYS berarti key yang sudah kompromi itu tetap hidup dan tetap valid. Selama masih ada di daftar itu, Laravel masih akan menggunakannya untuk mendekripsi data Crypt lama dan menerima cookie/session yang ditandatangani dengan key tersebut. Artinya, penyerang yang sempat menyalin key-mu tetap bisa membaca data terenkripsi dan memalsukan session — persis yang tidak kamu inginkan dari sebuah secret yang sudah dianggap mati.

Untuk kasus kebocoran yang benar-benar terkonfirmasi, menyimpan key lama di APP_PREVIOUS_KEYS bertentangan dengan prinsip yang kita bahas di awal: kredensial yang bocor harus mati. Pilihan yang lebih jujur secara keamanan: regenerasi key baru dan terima bahwa data terenkripsi lama perlu dibuat ulang — session dan cookie lama cukup dibiarkan terputus, pengguna tinggal log in lagi. Ini memang lebih menyakitkan dibanding jalur migrasi bertahap, tapi itu harga dari membunuh key yang sudah terekspos.

Kalau kamu tetap memilih jalur APP_PREVIOUS_KEYS (atau tidak punya pilihan karena datanya benar-benar perlu didekripsi), alurnya:

  1. Ambil APP_KEY yang bocor saat ini.
  2. Jalankan php artisan key:generate untuk menghasilkan key baru.
  3. Pasang key baru di APP_KEY, dan letakkan key lama di APP_PREVIOUS_KEYS.
  4. Buat script migrasi data — bisa dijalankan sebagai background job — untuk mendekripsi kolom dengan key lama lalu mengenkripsinya kembali dengan key baru, secara bertahap.
  5. Setelah semua data sudah terenkripsi ulang dengan key baru, segera hapus key lama dari APP_PREVIOUS_KEYS — jangan dibiarkan mengendap lebih lama dari yang dibutuhkan.

Dengan cara ini, aplikasi tidak langsung rusak saat key diganti. Data lama tetap terbaca selama proses migrasi berjalan, dan kamu bisa menuntaskannya dengan tenang dibanding harus do-all dalam satu malam. Tapi ingat dengan jalur ini: selama key lama masih di APP_PREVIOUS_KEYS, key yang sudah bocor itu tetap berfungsi. Semakin cepat kamu mencabutnya setelah migrasi selesai, semakin pendek jendela risikonya.

Kesimpulan

Membocorkan .env ke Git bukan akhir dunia, tapi juga bukan masalah yang bisa dibereskan dengan satu perintah. Ringkasnya, alurnya begini:

  1. Rotasi secret dulu — anggap semua isi .env lama sudah bocor, jangan dibiarkan hidup.
  2. Berhenti melacak dengan git rm --cached .env dan tambahkan ke .gitignore.
  3. Bersihkan riwayat dengan git-filter-repo (rekomendasi) atau BFG.
  4. Force push dan pastikan seluruh tim melakukan clone ulang agar tidak rekontaminasi.
  5. Garbage collect lokal dan hubungi GitHub Support untuk membersihkan cache server (dan sadari batasan fork).
  6. Untuk Laravel, urus rotasi APP_KEY. APP_PREVIOUS_KEYS hanya cocok untuk rotasi rutin; kalau key-nya benar-benar bocor, jangan mengabadikan key lama — regenerasi key baru dan biarkan data lama dibuat ulang.

Dan satu hal jujur yang perlu dipahami sejak awal: membersihkan history tidak mengembalikan rahasia yang sudah terlanjur bocor. Kalau repository-mu publik, atau secret-nya sempat aktif, perlakukan semua key itu sebagai kompromi apa pun yang kamu lakukan pada git-nya. Itu sebabnya rotasi kredensial — bukan pembersihan history — adalah langkah yang paling menentukan keamananmu.

Untuk mencegah hal ini terulang, selain .gitignore yang benar (yang sudah kita bahas), tambahkan lapisan proteksi: pre-commit hook seperti gitleaks yang memindai secret sebelum commit, dan aktifkan push protection di GitHub. Jauh lebih murah mencegah daripada membersihkan jejak.

Bagikan Artikel:
Diskusi & Komentar

Fitur komentar belum diaktifkan oleh administrator.

Artikel Terkait

Selesai membaca? Kembali ke beranda untuk melihat artikel menarik lainnya.

Kembali ke Beranda