Menyimpan Rahasia Aplikasi Java dengan Aman: Env, File, Vault

Rahasia aplikasi seperti password database, API key layanan kurir, atau secret HMAC untuk webhook tidak boleh berada di kode sumber maupun di repositori Git. Aturan praktisnya: kode hanya tahu nama rahasia, sedangkan nilainya disuntikkan saat aplikasi berjalan, lewat environment variable, file yang dipasang di luar repo, atau secret manager.

Untuk satu aplikasi dan tim kecil, environment variable yang dikelola dengan disiplin sudah cukup. Begitu layanan dan orangnya bertambah, secret manager menghemat banyak masalah. Artikel ini membahas pilihan penyimpanan, cara membacanya di Java, rotasi, dan kebiasaan kecil yang mencegah rahasia bocor ke log.

Bedakan konfigurasi biasa dan rahasia

Tidak semua konfigurasi perlu diperlakukan sama. Nama queue, URL broker, batas timeout, dan ukuran thread pool adalah konfigurasi biasa: boleh masuk repo, boleh terlihat di log, dan berguna saat debugging. Rahasia adalah nilai yang memberi akses: password, token, private key, client secret OAuth, dan kunci tanda tangan webhook. Bila nilai itu jatuh ke tangan orang lain, mereka bisa bertindak atas nama aplikasi Anda.

Pemisahan ini membuat keputusan lebih mudah. File application.yml boleh berisi struktur konfigurasi lengkap, tetapi bagian rahasia hanya berisi placeholder yang diisi dari luar.

broker:
  url: tcp://broker.internal:61616
  username: app-orders
  password: ${BROKER_PASSWORD}
kurir:
  base-url: https://api.kurir.example
  api-key: ${KURIR_API_KEY}

Sintaks ${...} di atas dikenali Spring Boot. Bila Anda tidak memakai Spring, polanya tetap sama: baca nilai dari sumber luar, dan jangan menulisnya di file yang ikut di-commit.

Tiga tempat menyimpan rahasia

1. Environment variable

Ini cara paling sederhana dan didukung hampir semua platform: systemd, Docker, Kubernetes, sampai layanan PaaS. Di Java, nilainya dibaca dengan System.getenv.

public final class Secrets {
    private Secrets() {}

    public static String required(String name) {
        String value = System.getenv(name);
        if (value == null || value.isBlank()) {
            throw new IllegalStateException(
                "Environment variable " + name + " belum diisi");
        }
        return value;
    }
}

// pemakaian
String apiKey = Secrets.required("KURIR_API_KEY");

Pesan error di atas hanya menyebut nama variabel, tidak pernah nilainya. Gagal cepat saat startup jauh lebih baik daripada aplikasi berjalan dengan nilai kosong lalu error di tengah transaksi pembeli.

Kelemahannya: environment variable bisa terbaca oleh proses lain milik user yang sama, ikut tercetak bila ada skrip debugging yang menjalankan env, dan sering tersalin ke file .env yang tidak sengaja di-commit. Tambahkan .env ke .gitignore sejak hari pertama.

2. File di luar repositori

Pilihan ini cocok untuk nilai panjang seperti private key, keystore, atau sertifikat. File diletakkan di direktori seperti /etc/app-orders/ dengan izin baca hanya untuk user aplikasi, misalnya chmod 600. Kubernetes Secret dan Docker secret juga lazim dipasang sebagai file.

import java.nio.file.Files;
import java.nio.file.Path;

Path path = Path.of(System.getenv().getOrDefault(
        "WEBHOOK_SECRET_FILE", "/run/secrets/webhook_secret"));
String webhookSecret = Files.readString(path).strip();

Keuntungan file: nilainya tidak muncul di daftar environment proses, dan bisa diganti tanpa mengubah definisi deployment. Pola “lokasi file lewat environment variable, isi lewat file” juga banyak dipakai image Docker resmi.

3. Secret manager

Secret manager adalah layanan khusus penyimpan rahasia, misalnya HashiCorp Vault atau layanan bawaan penyedia cloud. Kelebihannya ada pada kontrol akses per layanan, catatan audit siapa membaca apa, versi rahasia, dan dukungan rotasi. Aplikasi biasanya mengambil rahasia saat startup atau secara berkala dengan identitas mesin atau service account, sehingga tidak perlu menyimpan password untuk mengambil password.

Bila belum siap mengoperasikan secret manager, mulai dari environment variable yang diisi oleh pipeline deployment. Yang dicari adalah satu sumber kebenaran, bukan salinan rahasia yang tersebar di laptop tiap developer dan grup chat.

Perbandingan singkat

Cara Cocok untuk Catatan
Environment variable Aplikasi tunggal, tim kecil, kontainer Mudah, tetapi rawan tercetak dan tersalin ke .env
File di luar repo Private key, keystore, sertifikat Atur izin file; pastikan tidak ikut backup yang bisa dibaca umum
Secret manager Banyak layanan, banyak tim, kebutuhan audit Perlu dioperasikan; mendapat versi, audit, dan rotasi

Rotasi tanpa downtime

Rotasi berarti mengganti rahasia secara berkala, atau segera setelah ada dugaan bocor. Masalahnya, bila aplikasi hanya mengenal satu nilai, penggantian memaksa semua instans restart serentak. Pola yang lebih aman adalah mendukung dua nilai sekaligus selama masa transisi.

Untuk verifikasi tanda tangan webhook, misalnya, terima tanda tangan yang cocok dengan kunci lama atau kunci baru. Setelah pengirim sudah memakai kunci baru, kunci lama dihapus dari daftar.

boolean valid(String payload, String signature, List<String> activeSecrets) {
    for (String secret : activeSecrets) {
        if (HmacVerifier.matches(payload, signature, secret)) {
            return true;
        }
    }
    return false;
}

HmacVerifier di sini adalah class milik Anda sendiri yang menghitung HMAC lalu membandingkannya dengan MessageDigest.isEqual agar lama perbandingan tidak membocorkan informasi. Detail verifikasi tanda tangan dibahas di artikel webhook vs polling.

Untuk password database urutannya serupa: buat password atau user baru, deploy aplikasi yang memakai nilai baru, tunggu sampai koneksi lama habis dari connection pool, baru cabut nilai lama.

Jangan biarkan rahasia masuk log

Kebocoran paling sering justru datang dari log, bukan dari repo. Log dikirim ke banyak tempat, disimpan lama, dan dibaca lebih banyak orang daripada kode. Beberapa kebiasaan yang membantu:

  • Jangan log objek konfigurasi utuh. Bila perlu dicetak, override toString() agar field rahasia tampil sebagai ****.
  • Jangan log header Authorization atau body request ke API pihak ketiga tanpa penyaringan.
  • Record di Java otomatis membuat toString() yang memuat semua field. Override bila record berisi rahasia.
  • Waspadai pesan error library HTTP yang mencetak URL lengkap, terutama bila API key dikirim sebagai query parameter.
public record KurirConfig(String baseUrl, String apiKey) {
    @Override
    public String toString() {
        return "KurirConfig[baseUrl=" + baseUrl + ", apiKey=****]";
    }
}

Bila log Anda berbentuk JSON, aturan penyaringan bisa dipusatkan di satu tempat. Cara menyusun log seperti itu ada di artikel logging terstruktur dan correlation ID.

Kesalahan umum

  • Menghapus rahasia dari commit terakhir lalu merasa aman. Riwayat Git tetap menyimpannya. Anggap rahasia itu sudah bocor, lalu rotasi.
  • Satu API key untuk development, staging, dan produksi. Pisahkan per lingkungan agar kebocoran di laptop tidak membuka akses ke produksi.
  • Menaruh rahasia di image Docker. Layer image bisa dibaca siapa pun yang punya akses ke registry. Suntikkan nilainya saat container berjalan.
  • Memberi nilai default untuk rahasia. Default seperti changeme membuat aplikasi tetap jalan dengan nilai lemah. Lebih baik aplikasi menolak start.
  • Tidak punya daftar rahasia. Catat nama rahasia, pemilik, lokasi, dan tanggal rotasi terakhir. Tanpa daftar ini, rotasi darurat akan kacau.

Untuk proyek yang sudah berjalan, langkah awal yang realistis adalah mencari string mencurigakan di repo (kata seperti password, secret, atau token), memindahkan nilainya ke environment variable atau file, merotasi semua yang pernah ter-commit, lalu memasang pemindai rahasia di pipeline CI agar kejadian serupa tertangkap sebelum merge.

Hendra Kurniawan

Oleh Hendra Kurniawan

Pengembang backend Java yang lama menangani integrasi sistem dan message broker. Menulis catatan praktis tentang JMS, API, dan arsitektur aplikasi.