Mengelola secret di Git dengan Mozilla SOPS: enkripsi age, PGP, dan cloud KMS, konfigurasi .sops.yaml, integrasi decryption otomatis FluxCD, key rotation, serta praktik terbaik audit secret.

Di episode 19 kalian sudah mengamankan Flux: autentikasi, RBAC, network security, dan supply chain. Namun ada celah yang sengaja kita tinggalkan: secret seperti kunci SSH, password registry, dan token Helm masih ditulis mentah di manifest Git. Repo Git yang jadi sumber kebenaran GitOps berubah menjadi sumber kebocoran.
Episode ini membahas solusinya: Mozilla SOPS (Secrets OPerationS). SOPS mengenkripsi nilai-nilai sensitif di dalam file YAML atau JSON sehingga secret bisa di-commit ke Git tanpa membocorkan isinya, lalu Flux mendekripsinya secara otomatis saat menerapkan manifest.
SOPS adalah tool CLI untuk enkripsi file pada level field. Berbeda dengan Vault yang menyimpan secret terpusat, SOPS tidak punya server — ia murni memproses file. Ciri khasnya:
Prinsip dasarnya: Git menyimpan ciphertext, kunci privat tidak pernah masuk Git. Pada contoh episode 19, kita ganti password: ghp_xxxx menjadi blok terenkripsi ENC[AES256_GCM,data:...].
SOPS memakai envelope encryption: kunci data file (data key) dienkripsi oleh satu atau lebih kunci utama (master key). Pilihan master key menentukan alur kerjanya.
| Master key | Keuntungan | Cocok untuk |
|---|---|---|
| age | Ringan, tanpa server, satu file key | Tim kecil, on-premise |
| PGP | Standar lama, dukungan luas | Organisasi yang sudah pakai GPG |
| AWS KMS | Integrasi IAM, audit CloudTrail | Tim di AWS |
| GCP KMS | Workload Identity | Tim di GCP |
| Azure Key Vault | Managed identity | Tim di Azure |
Untuk lokal, age paling praktis. Buat kunci dengan age-keygen -o age.agekey, simpan kunci privat di tempat aman, dan hanya publikkan kunci publik ke .sops.yaml.
Tip
Umumkan kunci publik ke dalam .sops.yaml bukan rahasia — kunci publik memang untuk dibagikan. Yang harus dirahasiakan adalah file age.agekey atau kunci privat PGP.
Aturan enkripsi dideklarasikan di .sops.yaml di root repo. Aturan ini mencocokkan path file dan menentukan recipient atau KMS mana yang digunakan:
creation_rules:
- path_regex: clusters/.*/secrets\.yaml$
age: >-
age1q2w3e4r5t6y7u8i9o0p1a2s3d4f5g6h7j8k9l0zxc1v
encrypted_regex: "^(password|token|apiKey|secret)$"
- path_regex: clusters/.*/\.env$
age: >-
age1q2w3e4r5t6y7u8i9o0p1a2s3d4f5g6h7j8k9l0zxc1vencrypted_regex membatasi field mana yang dienkripsi, misalnya hanya password, token, apiKey, dan secret.
Dengan aturan di atas, enkripsi cukup satu perintah. Enkripsi semua field atau hanya field tertentu:
sops -e clusters/prod/secrets.yaml > clusters/prod/secrets.enc.yaml
sops --encrypt --encrypted-regex '^(password|token)$' \
clusters/prod/env.yaml > clusters/prod/env.enc.yaml
sops --encrypt-edit clusters/prod/secrets.yamlVersi lebih sederhana: beri nama file secrets.enc.yaml lalu jalankan sops --encrypt clusters/prod/secrets.enc.yaml — SOPS membaca .sops.yaml, memuat file, dan menulis ciphertext langsung ke file yang sama.
Bila hanya sebagian nilai yang rahasia, biarkan metadata lain terbaca agar diff tetap nyaman:
apiVersion: v1
kind: Secret
metadata:
name: api-db
namespace: apps
type: Opaque
stringData:
DB_HOST: postgres.prod.svc.cluster.local
DB_USER: app
DB_PASSWORD: ENC[AES256_GCM,data:0Nv6Y8j...,iv:...,tag:...,type:str]
sops:
kms: []
age:
- recipient: age1q2w3e4r5t6y7u8i9o0p1a2s3d4f5g6h7j8k9l0zxc1v
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
...Nilai non-rahasia seperti DB_HOST tetap terbaca; hanya DB_PASSWORD yang menjadi ciphertext. Blok sops: berisi metadata kunci untuk dekripsi dan audit.
Commit file terenkripsi seperti biasa; tim bisa melihat struktur tanpa melihat nilainya. Selalu verifikasi tidak ada plaintext yang lolos dengan memindai repo, misalnya memakai GitLeaks di CI, sebelum merge.
Flux mendekripsi secret secara otomatis. Kustomize controller memiliki dukungan SOPS bawaan; yang dibutuhkan hanyalah kunci privat yang tersedia sebagai Secret di cluster.
Pada Flux versi lama, dekripsi diaktifkan eksplisit lewat field decryption:
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: apps
namespace: flux-system
spec:
interval: 5m
sourceRef:
kind: GitRepository
name: apps
path: ./clusters/prod/apps
prune: true
decryption:
provider: sops
secretRef:
name: sops-agePada Flux 2.2 ke atas, decryption otomatis: cukup sediakan Secret sops-age (untuk age) atau sops-gpg (untuk PGP) di namespace flux-system, dan kustomize-controller mendekripsinya sendiri tanpa konfigurasi tambahan.
Untuk AWS KMS, jangan menaruh kunci di cluster. Gunakan IAM Roles for Service Accounts: beri role akses ke KMS key, lalu pasang annotation di ServiceAccount kustomize-controller. Flux lalu mendekripsi langsung lewat KMS tanpa kunci privat lokal.
Important
Kunci privat age hanya ada di cluster, bukan di Git. Kalau repo ter-expose, attacker mendapat ciphertext tanpa kunci — secret tetap aman. Inilah esensi GitOps yang aman.
Rotasi kunci membutuhkan enkripsi ulang semua file. SOPS menyediakan perintah khusus:
sops updatekeys clusters/prod/secrets.enc.yaml
sops --rotate --in-place clusters/prod/secrets.enc.yamlupdatekeys mengganti recipient kunci; --rotate membuat data key baru. Keduanya harus dijalankan saat kunci bocor atau saat pergantian environment.
age.agekey — tambahkan ke .gitignore dan jangan pernah di-commit.Episode ini menutup celah dari episode 19: secret kini bisa hidup di Git sebagai ciphertext. SOPS mengenkripsi file secara field-level, Flux mendekripsinya otomatis di cluster, dan kunci privat tidak pernah menyentuh repo. Kombinasi ini membuat secret dapat di-review, di-diff, dan diaudit seperti kode biasa.
Inti yang harus dibawa pulang:
.sops.yaml.sops-age atau sops-gpg, atau pakai IAM dan Workload Identity untuk KMS.sops updatekeys dan sops --rotate menjaga secret tetap aman.SOPS menjaga secret di dalam Git, tapi masih mengharuskan kunci ada di tiap cluster. Di episode 21 selanjutnya kita belajar External Secrets Operator: secret disimpan di sistem eksternal seperti AWS Secrets Manager, Vault, atau 1Password, dan Flux hanya mengelola referensinya. Sampai jumpa di episode 21!