Secret Management Best Practice #

Mengelola data sensitif (secrets) di dalam kluster Kubernetes lingkungan produksi bukanlah sekadar membuat objek Secret, menyimpannya ke dalam sistem, dan berharap semuanya aman. Pada konfigurasi bawaan kluster, objek Secret hanya disandingkan dengan pengodean Base64 yang dapat diterjemahkan kembali secara instan oleh siapa saja. Selain itu, database etcd secara default menyimpan data tersebut dalam bentuk teks biasa (plain text), tidak ada mekanisme rotasi otomatis kredensial bawaan, dan tidak ada pencatatan audit log terperinci untuk mendeteksi siapa saja yang telah mengakses kredensial kita.

Untuk membangun infrastruktur kluster yang benar-benar aman, kita harus menerapkan prinsip pertahanan berlapis (defense-in-depth). Kita wajib melindungi data sensitif mulai dari tingkat penyimpanan fisik, tingkat otorisasi API Server, hingga tingkat siklus hidup rahasia itu sendiri di dalam kode aplikasi. Artikel ini membahas secara mendalam praktik-praktik terbaik untuk mengamankan, mengaudit, menyinkronkan, dan merotasi Secret di lingkungan produksi kluster Kubernetes kita.


Lima Layer Keamanan Secret #

Dalam merancang keamanan Secret, kita tidak boleh hanya mengandalkan satu mekanisme pertahanan tunggal. Jika satu lapisan pertahanan berhasil ditembus (misalnya hacker berhasil menyusup ke node worker), lapisan pertahanan lainnya harus tetap mampu mengamankan kredensial utama kita.

Mari kita perhatikan arsitektur lima lapisan keamanan terpadu di bawah ini yang harus kita terapkan di kluster produksi:

flowchart TD
    subgraph Layer1["Layer 1: Enkripsi at-Rest (Penyimpanan)"]
        etcd["etcd Database (Terenkripsi KMS)"]
    end

    subgraph Layer2["Layer 2: Enkripsi in-Transit (Transit)"]
        etcd -->|"TLS Enkripsi Mandiri"| APIServer["kube-apiserver"]
    end

    subgraph Layer3["Layer 3: Kontrol Akses (Otorisasi)"]
        APIServer -->|"RBAC Least Privilege"| Client["kubectl / Pod ServiceAccount"]
    end

    subgraph Layer4["Layer 4: Audit & Monitoring (Pelacakan)"]
        APIServer -->|"Audit Logging Policy"| AuditLog["SIEM / Elasticsearch logs"]
    end

    subgraph Layer5["Layer 5: Manajemen Siklus Hidup (Rotasi)"]
        Client -->|"Rotasi Otomatis Dinamis"| App["Pod Kontainer Aplikasi"]
    end

Konfigurasi Enkripsi At-Rest Tingkat Lanjut #

Secara default, data yang disimpan oleh API Server ke dalam etcd tidak dienkripsi. Jika hacker berhasil mencuri backup berkas etcd atau membobol kontrol plane, seluruh kata sandi produksi kita akan terekspos. Oleh karena itu, mengaktifkan Enkripsi at-Rest adalah langkah wajib pertama di kluster produksi.

Contoh Manifes EncryptionConfiguration di Kluster Mandiri (Self-Managed) #

Pada kluster yang dikelola secara mandiri (misalnya menggunakan kubeadm), kita harus membuat berkas konfigurasi /etc/kubernetes/enc.yaml di kontrol plane dan merujuknya pada konfigurasi API Server:

apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
  - secrets
  providers:
  - aescbc:  # Gunakan penyedia AES-CBC untuk performa enkripsi kernel yang cepat
      keys:
      - name: key1
        secret: bXktc3VwZXItc2VjcmV0LWtleS1tdXN0LWJlLTMyLWJ5dGVzLXV0Zjg=  # Kunci Base64 32-byte
  - identity: {}  # Cadangan untuk membaca Secret lama yang belum terenkripsi

Prosedur Rewrite Data Setelah Aktivasi Enkripsi #

[!WARNING] Ketika kita mengaktifkan enkripsi di API Server, Kubernetes tidak secara otomatis mengenkripsi Secret yang sudah ada di dalam database etcd. Kebijakan enkripsi ini hanya berlaku untuk data Secret baru yang ditulis setelah konfigurasi diaktifkan.

Untuk mengenkripsi seluruh Secret lama yang sudah terlanjur tersimpan di etcd secara retroaktif, kita wajib menjalankan perintah pembacaan dan penulisan ulang (rewrite) secara massal:

# Perintah untuk memaksa menulis ulang seluruh Secret di semua namespace kluster
# Tindakan replace ini akan memicu API Server untuk menulis kembali data ke etcd menggunakan kunci enkripsi aktif
kubectl get secrets -A -o json | kubectl replace -f -

Membatasi Akses Menggunakan RBAC Tingkat Granular #

Kesalahan umum yang sering terjadi di tim DevOps adalah memberikan izin pembacaan Secret secara luas pada namespace tertentu. Hak akses get dan list pada objek Secret tidak boleh diberikan kepada developer non-admin atau ServiceAccount aplikasi yang tidak membutuhkannya.

Perbandingan Manifes Otorisasi RBAC #

Berikut adalah perbedaan antara pemberian akses yang terlalu longgar dan berbahaya (anti-pattern) dengan pembatasan hak akses spesifik menggunakan filter resourceNames (solusi):

# ANTI-PATTERN: Memberikan hak akses penuh ke seluruh Secret di namespace production
# ✗ Pod aplikasi dapat membaca API keys pembayaran dan database password milik aplikasi lain
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: generic-app-reader
  namespace: production
rules:
- apiGroups: [""]
  resources: ["secrets"]
  verbs: ["get", "list", "watch"]  # ✗ Terlalu permisif, memberikan akses ke seluruh Secret
---
# BENAR: Batasi hak akses hanya untuk get pada nama Secret tertentu
# ✓ Aman, membatasi verbs hanya ke 'get' (tidak bisa list) dan mengunci nama Secret secara eksplisit
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: specific-db-reader
  namespace: production
rules:
- apiGroups: [""]
  resources: ["secrets"]
  resourceNames: ["postgres-db-creds"]  # ✓ Mengunci nama Secret secara spesifik
  verbs: ["get"]  # ✓ Tidak boleh list/watch untuk mencegah perayapan data

Untuk mengaudit dan memastikan bahwa hak akses ServiceAccount aplikasi kita sudah terisolasi dengan benar, gunakan utilitas perintah diagnostik berikut:

# Uji apakah ServiceAccount tertentu memiliki izin membaca Secret di namespace production
kubectl auth can-i get secrets -n production \
  --as=system:serviceaccount:production:my-app-sa

# Output yang diharapkan: "no"

Sinkronisasi Rahasia Eksternal: Integrasi dengan Cloud Secret Manager #

Menyimpan nilai Secret Base64 secara langsung di dalam manifes YAML yang di-commit ke repositori Git adalah pelanggaran keamanan yang fatal dalam prinsip GitOps. Untuk mengatasinya, kita harus menggunakan pengelola rahasia eksternal (External Secret Manager) seperti AWS Secrets Manager, Google Cloud Secret Manager, atau HashiCorp Vault.

Alat terbaik untuk menyinkronkan data dari cloud manager ke dalam kluster Kubernetes adalah External Secrets Operator (ESO).

Contoh Implementasi External Secrets Operator (ESO) dengan AWS #

Kita dapat mendefinisikan objek SecretStore untuk menghubungkan kluster ke layanan AWS Secrets Manager menggunakan autentikasi ServiceAccount yang terikat peran IAM (IRSA):

apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
  name: aws-secrets-manager-store
  namespace: production
spec:
  provider:
    aws:
      service: SecretsManager
      region: ap-southeast-1
      auth:
        jwt:
          # Merujuk ke ServiceAccount lokal yang terikat dengan peran IAM Role AWS
          serviceAccountRef:
            name: eso-aws-auth-sa
---
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: sync-db-credentials
  namespace: production
spec:
  refreshInterval: "1h"  # Lakukan sinkronisasi data dari AWS setiap 1 jam
  secretStoreRef:
    name: aws-secrets-manager-store
    kind: SecretStore
  target:
    name: local-db-secret  # Nama objek Kubernetes Secret yang akan dibuat secara otomatis
    creationPolicy: Owner
  data:
  - secretKey: db-password  # Kunci tujuan di Kubernetes Secret
    remoteRef:
      key: production/rds/postgres  # Nama kunci di AWS Secrets Manager
      property: password  # Properti JSON spesifik di AWS

Dengan pola ini, repositori Git kita hanya menyimpan manifes ExternalSecret yang aman (tidak berisi nilai sensitif). Nilai rahasia aktual tetap berada di cloud provider dan disinkronkan secara otomatis ke kluster.

Alternatif: HashiCorp Vault dengan Vault Agent Injector #

Jika kita menggunakan HashiCorp Vault sebagai server pengelola rahasia terpusat, kita memiliki opsi untuk memproyeksikan rahasia langsung ke dalam sistem berkas (filesystem) Pod tanpa pernah menyimpannya sebagai objek Secret di dalam database etcd kluster. Pendekatan ini dilakukan menggunakan Vault Agent Injector yang bekerja berdasarkan anotasi kontainer.

Ketika API Server mendeteksi anotasi spesifik Vault pada Pod, ia akan menyuntikkan kontainer init (init container) dan kontainer sidecar Vault Agent yang secara otomatis mengambil rahasia dari Vault dan menulisnya ke direktori memori bersama /vault/secrets/.

Berikut adalah contoh manifes Deployment dengan konfigurasi anotasi Vault Agent:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: billing-gateway
  namespace: production
spec:
  replicas: 2
  selector:
    matchLabels:
      app: billing-gateway
  template:
    metadata:
      labels:
        app: billing-gateway
      annotations:
        # Mengaktifkan penyuntikan agen Vault
        vault.hashicorp.com/agent-inject: "true"
        # Menentukan peran (role) autentikasi yang diizinkan di Vault
        vault.hashicorp.com/role: "billing-app-role"
        # Menentukan path rahasia di Vault dan nama file tujuan di dalam Pod
        vault.hashicorp.com/agent-inject-secret-db-config: "secret/data/production/db"
        # Menulis templat kustom untuk menyusun kredensial ke format file env
        vault.hashicorp.com/agent-inject-template-db-config: |
          {{- with secret "secret/data/production/db" -}}
          export DB_USER="{{ .Data.data.username }}"
          export DB_PASSWORD="{{ .Data.data.password }}"
          export DB_URL="postgresql://{{ .Data.data.username }}:{{ .Data.data.password }}@postgres-master:5432/billingdb"
          {{- end }}          
    spec:
      containers:
      - name: gateway-app
        image: billing-app:v3.0.0
        command: ["/bin/sh", "-c", "source /vault/secrets/db-config && ./run-app"]

Dengan pola Vault Agent Injector:

  • Rahasia hanya ada di memori RAM kontainer yang sedang berjalan.
  • Data rahasia tidak pernah tersimpan di database etcd Kubernetes, menghilangkan risiko kebocoran data jika backup etcd disusupi.
  • Rotasi rahasia di Vault akan langsung menulis ulang berkas /vault/secrets/db-config secara real-time di dalam kontainer.

Strategi Rotasi Kredensial Otomatis Tanpa Downtime #

Menjaga satu kata sandi database tetap sama selama bertahun-tahun di produksi adalah risiko keamanan yang tinggi. Kita harus melakukan rotasi kredensial secara berkala. Namun, melakukan rotasi manual sering kali memicu downtime aplikasi karena adanya jeda waktu antara perubahan password di database dengan pembaruan di aplikasi.

Untuk melakukan rotasi tanpa downtime, kita wajib mengadopsi Pola Kredensial Ganda (Double-Credentials Pattern).

ALUR ROTASI KREDENSIAL TANPA DOWNTIME:

  Langkah 1: Database dikonfigurasi untuk menerima dua credential aktif sekaligus:
             - Credential A (Aktif digunakan oleh Pod lama)
             - Credential B (Baru dibuat untuk proses transisi)
  
  Langkah 2: Perbarui Secret di External Secret Manager dengan Credential B
  Langkah 3: ESO mendeteksi perubahan dan menyinkronkan Credential B ke kluster
  Langkah 4: Picu rolling update Deployment:
             - Pod baru aktif dan mulai menggunakan Credential B
             - Pod lama tetap berjalan menggunakan Credential A
  
  Langkah 5: Setelah seluruh Pod baru sehat, hapus Credential A dari database

Konfigurasi Sinkronisasi Rotasi Cepat #

Kita dapat menyetel parameter refreshInterval pada manifes ExternalSecret ke durasi yang lebih cepat untuk mempercepat deteksi perubahan kredensial:

spec:
  refreshInterval: "15m"  # Sinkronisasikan versi baru dalam waktu maks 15 menit

Pola GitOps Aman untuk Rahasia (Secrets-as-Code) #

Bagi organisasi yang menerapkan GitOps penuh di mana seluruh konfigurasi kluster wajib direpresentasikan di Git, ada tiga opsi populer untuk mengelola Secret tanpa membocorkan datanya:

1. Sealed Secrets (Bitnami) #

Sealed Secrets menggunakan kriptografi asimetris untuk mengamankan data. Kita mengenkripsi objek Secret menggunakan perkakas CLI kubeseal di komputer lokal dengan memanfaatkan kunci publik (public key) yang diperoleh dari controller Sealed Secrets yang berjalan di dalam kluster.

# Mengonversi Secret standar menjadi SealedSecret terenkripsi
# Hasil enkripsi ini sangat aman di-commit ke repositori Git publik sekalipun
kubeseal --controller-name=sealed-secrets --format=yaml < db-secret.yaml > db-sealedsecret.yaml

Hasil enkripsinya berupa berkas manifes YAML kustom SealedSecret yang dapat di-commit ke Git:

apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
  name: production-db-secret
  namespace: production
spec:
  encryptedData:
    # Hanya controller Sealed Secrets di dalam kluster yang memiliki private key untuk mendekripsi data ini
    database-password: AgBy8BbKJk9p8Y8SbOzU23hY7gHGtT2...

2. Mozilla SOPS (Secrets Operations) #

SOPS adalah editor berkas teks terenkripsi yang mendukung format YAML, JSON, ENV, INI, dan BIN. SOPS mengintegrasikan proses enkripsi dengan cloud KMS (seperti AWS KMS, GCP KMS, Azure Key Vault, atau HashiCorp Vault) atau GPG lokal.

Keunggulan SOPS adalah ia hanya mengenkripsi nilai data (values) saja, sedangkan struktur kunci (keys) dan metadata manifest Kubernetes tetap dibiarkan berupa plain text agar kita mudah membaca kerangka manifes tersebut di Git.

# Mengenkripsi file secret menggunakan AWS KMS key
sops --encrypt --kms "arn:aws:kms:ap-southeast-1:123456789012:key/xxxx" secret.yaml > secret.enc.yaml

Kebijakan Audit Logging dan Monitoring Akses Secret #

Keamanan Secret tidak lengkap tanpa adanya pelacakan akses (audit trail). Kita harus tahu siapa saja (pengguna atau ServiceAccount) yang telah melakukan panggilan API untuk membaca Secret kluster kita guna mendeteksi aktivitas mencurigakan secara dini.

Kita dapat membuat aturan pemfilteran audit pada API Server dengan menulis berkas /etc/kubernetes/audit-policy.yaml:

apiVersion: audit.k8s.io/v1
kind: Policy
rules:
# 1. Catat seluruh permintaan penulisan (create, update, delete) pada Secret dengan level RequestResponse
- level: RequestResponse
  resources:
  - group: "" # Core API Group
    resources: ["secrets"]
  verbs: ["create", "update", "patch", "delete"]

# 2. Catat akses pembacaan (get, list, watch) oleh pengguna dengan level Metadata untuk efisiensi ukuran log
- level: Metadata
  resources:
  - group: ""
    resources: ["secrets"]
  verbs: ["get", "list", "watch"]

# 3. Abaikan logging kueri Secret oleh sistem internal kubelet untuk mengurangi log noise
- level: None
  users: ["system:node", "system:kube-scheduler", "system:kube-controller-manager"]
  resources:
  - group: ""
    resources: ["secrets"]

Log audit ini kemudian harus dikirimkan ke repositori log terpusat atau sistem SIEM (seperti Splunk atau Elasticsearch) untuk memicu peringatan (alert) jika terdeteksi kueri list secrets secara massal di luar jam kerja.


Ringkasan #

  • Aktifkan enkripsi at-rest etcd: Jangan biarkan database etcd menyimpan rahasia dalam format Base64 teks biasa; aktifkan enkripsi menggunakan KMS provider.
  • Lakukan rewrite setelah enkripsi aktif: Jalankan perintah replace massal untuk memaksa data Secret lama ditulis ulang ke etcd dalam format terenkripsi.
  • Terapkan RBAC granular: Gunakan parameter resourceNames untuk membatasi akses baca Secret hanya ke Pod spesifik yang berhak.
  • Gunakan External Secrets Operator: Hindari menyimpan representasi nilai Secret di Git; sinkronisasikan rahasia secara otomatis dari AWS/GCP/Vault menggunakan ESO.
  • Adopsi pola rotasi double-credentials: Lakukan rotasi kredensial database secara bertahap menggunakan dua kredensial aktif untuk menghindari downtime aplikasi selama transisi.
  • Konfigurasikan API Server Audit Policy: Catat setiap akses baca-tulis ke objek Secret di dalam log audit untuk mendeteksi potensi kebocoran data.

← Sebelumnya: Environment Variable Pattern   Berikutnya: External Secret Manager →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact