Aplikasi modern memerlukan berbagai rahasia: password database, API token, kunci enkripsi, dan kredensial layanan pihak ketiga. Menempelkan rahasia langsung di dalam source code tampak praktis, namun berdampak luas: rahasia ikut tercommit ke repositori, terbaca oleh siapa pun yang memiliki akses baca, dan sulit dirotasi tanpa deploy ulang. Praktik yang aman memisahkan rahasia dari kode, menyimpannya pada sumber eksternal yang terkontrol, dan membatasi akses berdasarkan peran. Pada chapter ini kita akan membangun lapisan pengelola rahasia bertingkat—environment variable, file .env lokal, dan Vault tiruan—untuk memahami pola yang sama dipakai pada sistem produksi seperti HashiCorp Vault atau AWS KMS.

Mengapa Hardcoding Berbahaya #

Hardcoding menempatkan rahasia pada repo Git, yang berarti:

  • Riwayat commit menyimpan setiap versi rahasia, bahkan setelah dihapus di commit berikutnya.
  • Setiap orang yang dapat membaca repo (termasuk kontraktor sementara) memperoleh rahasia produksi.
  • Rotasi rahasia memerlukan commit baru, deploy ulang, serta koordinasi tim.
  • Pemindahan ke layanan lain (misalnya ganti vendor database) memaksa pembaruan kode.
kotlin Anti-pattern: rahasia di dalam kode
// JANGAN LAKUKAN INI pada kode produksi:
// val dbPassword = "super-secret-12345"
// val apiToken = "sk_live_abcdef0123456789"

Pola yang benar: rahasia disimpan pada sumber eksternal, dan kode hanya membaca referensi berdasarkan kunci.

Sumber Rahasia Bertingkat #

Kita mulai dengan abstraksi SecretSource yang menyembunyikan detail sumber konkret. Implementasi bertingkat—env, file .env, lalu Vault—memungkinkan pengembang memakai .env lokal sementara produksi memakai Vault tanpa mengubah kode aplikasi.

kotlin Interface SecretSource
interface SecretSource {
    fun get(key: String): String?
}

Implementasi EnvSecretSource membaca System.getenv, DotEnvSecretSource membaca file .env sederhana, dan VaultSecretSource menyimulasikan akses ke Vault dengan mencatat setiap kunci yang dibaca. ChainedSecretSource menggabungkan ketiganya: sumber pertama yang mengembalikan nilai non-null menang.

kotlin
// secrets_management.kt
// Don't hardcode secrets. Env vars. Vault. KMS. kotlinx.secrets-gradle-plugin concept.

import java.io.File

// --- Sumber rahasia abstrak (interface) ---
interface SecretSource {
    fun get(key: String): String?
}

// --- Sumber dari environment variable ---
class EnvSecretSource : SecretSource {
    override fun get(key: String): String? = System.getenv(key)
}

// --- Sumber dari file .env sederhana (KEY=VALUE per baris) ---
class DotEnvSecretSource(private val path: String) : SecretSource {
    private val data: Map<String, String> = run {
        val f = File(path)
        if (!f.exists()) emptyMap()
        else f.readLines()
            .filter { it.isNotBlank() && !it.startsWith("#") && '=' in it }
            .associate {
                val (k, v) = it.split("=", limit = 2)
                k.trim() to v.trim().removeSurrounding("\"")
            }
    }
    override fun get(key: String): String? = data[key]
}

// --- Sumber mock Vault (mencatat akses) ---
class VaultSecretSource(private val store: Map<String, String>) : SecretSource {
    val akses: MutableList<String> = mutableListOf()
    override fun get(key: String): String? {
        akses.add(key)
        return store[key]
    }
}

// --- Rantai sumber: cek env, lalu .env, lalu Vault ---
class ChainedSecretSource(private val sources: List<SecretSource>) : SecretSource {
    override fun get(key: String): String? =
        sources.firstNotNullOfOrNull { it.get(key) }
}

// --- Penampung rahasia aman: tidak mencetak nilai mentah ---
class SecureConfig(private val source: SecretSource) {
    fun databasePassword(): String = source.get("DB_PASSWORD")
        ?: error("DB_PASSWORD tidak ditemukan di sumber manapun")

    fun apiToken(): String = source.get("API_TOKEN")
        ?: error("API_TOKEN tidak ditemukan di sumber manapun")

    fun masked(token: String): String {
        if (token.length <= 8) return "*".repeat(token.length)
        return token.take(4) + "*".repeat(token.length - 8) + token.takeLast(4)
    }
}

fun main() {
    // Sumber bertingkat: env (kosong di sini) -> .env (mock) -> Vault mock
    val vaultStore = mapOf("DB_PASSWORD" to "super-rahasia-12345", "API_TOKEN" to "sk_test_abcdef0123456789")
    val vault = VaultSecretSource(vaultStore)
    val chain = ChainedSecretSource(listOf(EnvSecretSource(), vault))

    val config = SecureConfig(chain)
    val dbPass = config.databasePassword()
    val apiToken = config.apiToken()

    println("DB_PASSWORD (masked): ${config.masked(dbPass)}")
    println("API_TOKEN   (masked): ${config.masked(apiToken)}")

    println("---")
    println("Akses Vault yang tercatat: ${vault.akses}")
}

Output yang Dihasilkan:

text
DB_PASSWORD (masked): supe***********2345
API_TOKEN   (masked): sk_t****************6789
---
Akses Vault yang tercatat: [DB_PASSWORD, API_TOKEN]

Mari kita bedah bagian-bagian kunci dari kode di atas:

  1. DotEnvSecretSource: membaca file .env baris-per-baris, melewati komentar (#) dan baris kosong, lalu memisahkan KEY=VALUE pada tanda = pertama. Tanda kutip di sekitar nilai dibersihkan dengan removeSurrounding("\""). Pendekatan sederhana ini cukup untuk pengembangan lokal; produksi memerlukan parser yang lebih ketat.
  2. VaultSecretSource: menyimulasikan Vault dengan Map<String, String> internal serta daftar akses yang mencatat setiap kunci dibaca. Pada Vault nyata, akses dicatat di audit log untuk kepatuhan.
  3. ChainedSecretSource: memakai firstNotNullOfOrNull untuk mengembalikan nilai pertama yang non-null dari rantai sumber. Urutan sumber penting: env (prioritas tertinggi), .env (fallback lokal), Vault (fallback produksi).
  4. SecureConfig: satu-satunya tempat kode aplikasi menyentuh rahasia. Fungsi databasePassword() dan apiToken() mengembalikan rahasia atau melempar exception dengan pesan yang menjelaskan sumber mana yang gagal. Pemanggil tidak perlu tahu detail sumber.
  5. masked(token): menampilkan rahasia dengan hanya empat karakter pertama dan terakhir, sisanya diganti *. Berguna untuk log dan debugging tanpa membocorkan nilai penuh.

Variabel Environment dan File .env #

Variabel environment adalah mekanisme paling sederhana untuk menyuntikkan rahasia ke proses aplikasi. Pada deployment container (Docker, Kubernetes), variabel ini diisi dari secret store yang dikelola orchestrator.

bash Contoh: menjalankan aplikasi dengan env var
export DB_PASSWORD="YOUR_DATABASE_PASSWORD"
export API_TOKEN="YOUR_API_TOKEN"
java -jar aplikasi.jar

File .env berguna untuk pengembangan lokal karena tidak ikut tercommit—tambahkan .env ke .gitignore. Library seperti dotenv-kotlin membaca file ini secara otomatis saat aplikasi start.

Vault dan KMS #

Pada skala produksi, rahasia dikelola oleh layanan khusus:

  • HashiCorp Vault: menyimpan rahasia terenkripsi, mendukung dynamic secrets (token dibuat saat diminta, kedaluwarsa otomatis), serta audit log lengkap.
  • AWS KMS / Google Cloud KMS: layanan manajemen kunci enkripsi. Aplikasi tidak pernah melihat kunci master—enkripsi/dekripsi terjadi di sisi layanan.
  • kotlinx.secrets-gradle-plugin adalah plugin Gradle yang menyuntikkan rahasia sebagai BuildConfig saat build time. Plugin ini cocok untuk rahasia yang tidak terlalu sensitif (misalnya API key layanan analitik).
kotlin Contoh: akses Vault melalui HTTP (ilustrasi)
// import io.ktor.client.*
// import io.ktor.client.request.*
//
// suspend fun readSecret(path: String, token: String): String {
//     return HttpClient().use { c ->
//         c.get("https://vault.example.com/v1/$path") {
//             header("X-Vault-Token", token)
//         }
//     }
// }

Kode di atas tidak dijalankan karena memerlukan Vault nyata. Pola yang sama berlaku: aplikasi membawa token otentikasi (bukan rahasia itu sendiri) untuk meminta rahasia dari Vault.

Rotasi dan Audit #

Rahasia yang baik dapat dirotasi tanpa mengubah kode aplikasi. Karena SecureConfig membaca dari SecretSource, rotasi cukup memperbarui nilai di Vault—aplikasi akan membaca nilai baru pada permintaan berikutnya. Audit log pada VaultSecretSource menunjukkan kunci mana yang dibaca, kapan, dan oleh instance aplikasi mana.

Referensi Lanjutan #

Penutup #

Pengelolaan rahasia yang baik memisahkan kode dari kredensial, mengelola akses berdasarkan peran, serta menyediakan jejak audit. Chapter berikutnya akan masuk ke kriptografi fundamental: algoritma simetris vs asimetris, fungsi hash, serta HMAC untuk integritas data.