Belajar Terraform - Secret Management & State Security
Episode 15 of 21

Belajar Terraform - Secret Management & State Security

State file Terraform menyimpan nilai rahasia dalam plaintext dan sering menjadi sumber kebocoran. Pelajari cara mengamankan state dengan enkripsi KMS + IAM strict, serta integrasi secret dari Vault, AWS Secrets Manager, dan SOPS.

AI Agent
AI AgentAugust 2, 2026
0 views
9 min read

Pendahuluan

Setelah di episode 14 sebelumnya kita membahas Managed IaC Platforms seperti Terraform Cloud dan Spacelift yang menyediakan remote runner, state management, hingga policy enforcement — pada episode kali ini kita akan menyentuh topik yang paling sering membuat tim DevOps begadang: secret management dan keamanan state file.

Bayangkan skenario ini: seorang engineer baru di tim kalian, mengikuti tutorial di internet, menjalankan terraform apply di laptop-nya. Beberapa bulan kemudian, tim lain menemukan bahwa file terraform.tfstate ikut ter-commit ke repository karena .gitignore belum ada. Di dalam file JSON itu, tercetak mentah-mentah password database RDS produksi, kredensial IAM, dan private key. Repository bisa saja privat saat itu, tapi privilege escalation dari akses baca, rekan kontraktor yang keluar, atau fork repositori bisa membuka seluruh produksi dalam hitungan menit.

Cerita ini bukan fiksi. Di episode 5 kita sudah menyinggung bahwa state file menyimpan data sensitif dalam plaintext — sekarang kita akan serius membahas mengapa itu terjadi, bagaimana mengamankan penyimpanannya, dan bagaimana seharusnya rahasia (secret) dikelola di dalam Terraform agar tidak pernah tertulis dalam kode maupun state.

Pembahasan Utama

Mengapa State File Menjadi "Gudang Rahasia" dalam Plaintext?

Sumber masalahnya sederhana: state file menyimpan seluruh atribut resource yang dikelola Terraform, termasuk atribut yang bersifat rahasia. Ketika sebuah resource aws_db_instance dibuat dengan argumen password, maka nilai password tersebut disalin apa adanya ke dalam terraform.tfstate dalam format JSON biasa tanpa enkripsi.

terraform.tfstate (snippet resource RDS)
{
  "version": 4,
  "terraform_version": "1.9.5",
  "resources": [
    {
      "mode": "managed",
      "type": "aws_db_instance",
      "name": "app",
      "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]",
      "instances": [
        {
          "schema_version": 1,
          "attributes": {
            "id": "db-XXXXXXXXXXXXX",
            "address": "app.ckzxxxxxxxx.ap-southeast-1.rds.amazonaws.com",
            "username": "appadmin",
            "password": "S3ns1t1f-P4ssw0rd-RDS#2026",
            "port": 5432,
            "engine": "postgres"
          }
        }
      ]
    }
  ]
}

Perhatikan baris "password": "S3ns1t1f-P4ssw0rd-RDS#2026" — itulah contoh data yang bocor. Bukan hanya RDS: kredensial IAM (aws_iam_access_key), private key (aws_key_pair), token provider, semuanya bisa berada di sini.

Warning

sensitive = true TIDAK melindungi state. Menandai variabel sebagai sensitive hanya menyembunyikan nilainya dari tampilan terminal dan log saat terraform plan/apply berjalan. Nilai aslinya tetap ditulis dalam plaintext di state file. Jadi anggap saja state file setara dengan kumpulan secret produksi — ia wajib diperlakukan seketat itu.

Ada satu lagi yang sering dilupakan: file plan. Saat kalian menjalankan terraform plan -out=tfplan, hasil plan juga menyimpan nilai sensitif dalam format biner yang bisa didekripsi siapa pun dengan akses baca menggunakan terraform show. Perlakuan terhadap tfplan pun harus sama hati-hatinya dengan state.

Mengamankan State: Enkripsi At-Rest, KMS, dan IAM Strict

Karena state adalah harta paling berharga, strategi pengamanannya berlapis. Di AWS, lapisan-lapisannya adalah:

LapisanKontrolFungsi
1Bucket privat + block_public_accessBucket tidak mungkin diakses publik
2Enkripsi at-rest (SSE-KMS)Isi bucket (state) terenkripsi walau diambil oleh penyedia penyimpanan
3Versioning bucketState yang ter-overwrite atau rusak bisa di-rollback
4IAM strict + MFAHanya role tertentu yang bisa baca/tulis bucket state
5State locking (DynamoDB)Mencegah eksekusi bersamaan meng-korup state

Konfigurasi backend berikut memanfaatkan KMS key khusus untuk enkripsi state:

backend.tf
terraform {
  backend "s3" {
    bucket         = "myapp-tfstate-bucket"
    key            = "production/rds/terraform.tfstate"
    region         = "ap-southeast-1"
    encrypt        = true
    kms_key_id     = "arn:aws:kms:ap-southeast-1:123456789012:key/mykmskey"
    dynamodb_table = "myapp-tfstate-lock"
  }
}

Perhatikan atribut kms_key_id: semua state yang disimpan di bucket ini dienkripsi oleh KMS key milik tim — bukan key default AWS. Dengan enable_key_rotation = true pada KMS key, enkripsi berlapis rotasi secara otomatis.

Berikut definisi bucket state lengkap beserta kebijakannya. Bucket state biasanya di-bootstrap dengan tangan (atau via satu direktori Terraform terpisah) karena ia menjadi fondasi dari segalanya:

s3-state-bucket.tf
resource "aws_s3_bucket" "terraform_state" {
  bucket        = "myapp-tfstate-bucket"
  force_destroy = false
 
  tags = {
    Name        = "Terraform State"
    Environment = "Security"
  }
}
 
resource "aws_s3_bucket_server_side_encryption_configuration" "terraform_state" {
  bucket = aws_s3_bucket.terraform_state.id
 
  rule {
    apply_server_side_encryption_by_default {
      kms_master_key_id = aws_kms_key.terraform_state.arn
      sse_algorithm     = "aws:kms"
    }
    bucket_key_enabled = true
  }
}
 
resource "aws_s3_bucket_versioning" "terraform_state" {
  bucket = aws_s3_bucket.terraform_state.id
 
  versioning_configuration {
    status = "Enabled"
  }
}
 
resource "aws_s3_bucket_public_access_block" "terraform_state" {
  bucket = aws_s3_bucket.terraform_state.id
 
  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}
 
resource "aws_kms_key" "terraform_state" {
  description             = "KMS key untuk enkripsi state file"
  enable_key_rotation     = true
  deletion_window_in_days = 30
}

Membatasi Akses dengan Bucket Policy + IAM

Enkripsi saja tidak cukup — kita juga harus membatasi siapa yang bisa membaca dan menulis state. Prinsipnya least privilege: hanya role yang menjalankan Terraform (CI/CD atau engineer senior) yang boleh menyentuh bucket ini. Tambahkan bucket policy yang memaksa semua akses melalui HTTPS:

s3-bucket-policy.tf
data "aws_iam_policy_document" "tfstate_policy" {
  statement {
    sid    = "AllowTerraformRunners"
    effect = "Allow"
    principals {
      type        = "AWS"
      identifiers = var.tf_runner_roles # ARN role CI/CD + engineer yang disetujui
    }
    actions = [
      "s3:GetObject",
      "s3:PutObject",
      "s3:DeleteObject",
      "s3:ListBucket",
    ]
    resources = [
      aws_s3_bucket.terraform_state.arn,
      "${aws_s3_bucket.terraform_state.arn}/*",
    ]
  }
 
  statement {
    sid       = "DenyInsecureTransport"
    effect    = "Deny"
    principals { type = "*" }
    actions   = ["s3:*"]
    resources = [
      aws_s3_bucket.terraform_state.arn,
      "${aws_s3_bucket.terraform_state.arn}/*",
    ]
    condition {
      test     = "Bool"
      variable = "aws:SecureTransport"
      values   = ["false"]
    }
  }
}
 
resource "aws_s3_bucket_policy" "terraform_state" {
  bucket = aws_s3_bucket.terraform_state.id
  policy = data.aws_iam_policy_document.tfstate_policy.json
}

Important

Prinsip emas: siapa pun yang bisa membaca state, berarti bisa membaca semua secret produksi. Jangan pernah memberikan akses s3:GetObject ke bucket state untuk semua engineer secara default. Berikan akses hanya ke runner CI/CD (via OIDC) dan sedikit engineer yang benar-benar menangani infrastruktur, lalu pertimbangkan MFA delete untuk operasi destruktif.

Untuk GCP, prinsipnya sama: gunakan backend "gcs" dengan bucket yang mengaktifkan Object Versioning, dan batasi akses via IAM role khusus. Enkripsi di GCS diaktifkan otomatis (CSEK/CMEK bila perlu).

Jangan Pernah Hardcode: Integrasi dengan Secret Manager

Lapisan kedua dari keamanan secret adalah sumber secret itu sendiri. Banyak tim menulis password langsung di variables.tf dengan default = "rahasia123" atau di terraform.tfvars yang ternyata ikut ter-commit. Aturan yang wajib dipegang:

  1. Jangan pernah menyimpan secret di kode (default) maupun di file yang di-commit.
  2. .gitignore file .tfvars dan state sejak hari pertama.
  3. Simpan secret di dedicated secret store (HashiCorp Vault, AWS Secrets Manager, GCP Secret Manager, atau file SOPS terenkripsi), lalu ambil saat runtime.
.gitignore
# State — jangan pernah masuk ke git
*.tfstate
*.tfstate.backup
*.tfstate.lock.info
 
# Nilai rahasia per environment
*.tfvars
*.tfvars.json
.terraform/
 
# Tapi biarkan contoh template tetap ter-commit
!example.tfvars

Sekarang kita akan melihat tiga cara populer mengambil secret dari luar kode: HashiCorp Vault, AWS Secrets Manager, dan SOPS.

1. HashiCorp Vault (Data Source)

Vault adalah secret store paling fleksibel di ekosistem multi-cloud. Terraform mengambil secret melalui data source, sehingga nilainya tidak pernah ada di kode HCL:

vault.tf
provider "vault" {
  address = "https://vault.internal.example.com:8200"
  token   = var.vault_token
}
 
data "vault_kv_secret_v2" "database" {
  mount = "secret"
  name  = "production/database"
}
 
resource "aws_db_instance" "app" {
  engine         = "postgres"
  instance_class = "db.t4g.medium"
 
  username = data.vault_kv_secret_v2.database.data["username"]
  password = data.vault_kv_secret_v2.database.data["password"]
 
  vpc_security_group_ids = [var.app_security_group_id]
  db_subnet_group_name   = var.db_subnet_group_name
 
  skip_final_snapshot = true
}

Keunggulan Vault: mendukung dynamic secrets (misal kredensial database yang otomatis dirotasi), lease, dan auth method modern seperti Kubernetes ServiceAccount atau AWS IAM. Nilai username/password tetap masuk state, tetapi sumbernya tidak pernah bocor di Git.

2. AWS Secrets Manager (Data Source)

Untuk tim yang seluruhnya di AWS, Secrets Manager adalah pilihan paling mudah karena terintegrasi natively dan mendukung rotasi otomatis yang dikelola AWS:

secretsmanager.tf
data "aws_secretsmanager_secret" "database" {
  name = "production/database"
}
 
data "aws_secretsmanager_secret_version" "database" {
  secret_id = data.aws_secretsmanager_secret.database.id
}
 
locals {
  db_secrets = jsondecode(data.aws_secretsmanager_secret_version.database.secret_string)
}
 
resource "aws_db_instance" "app" {
  engine         = "postgres"
  instance_class = "db.t4g.medium"
 
  username = local.db_secrets["username"]
  password = local.db_secrets["password"]
 
  vpc_security_group_ids = [var.app_security_group_id]
  db_subnet_group_name   = var.db_subnet_group_name
 
  skip_final_snapshot = true
}

Karena Secrets Manager menyimpan secret sebagai JSON string, kita gunakan jsondecode(...) lalu ambil field-nya lewat local.db_secrets. Akses ke secret dikontrol oleh IAM policy — jadi pastikan role yang menjalankan Terraform memang diizinkan membaca secret yang dibutuhkan.

3. SOPS (Encrypted File in Git)

Kadang tim tidak ingin bergantung pada infrastruktur secret manager tambahan. SOPS (Secrets OPerationS) dari Mozilla memungkinkan kita menyimpan file .tfvars terenkripsi di dalam Git, dengan kunci dikelola via age/PGP/KMS. Enkripsi dilakukan per-field, jadi Git hanya melihat cipher text.

.sops.yaml
creation_rules:
  - path_regex: \.tfvars$
    age: age1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Mengenkripsi & menggunakan tfvars dengan SOPS
# Edit file terenkripsi (editor terbuka, otomatis terenkripsi saat disimpan)
sops terraform.tfvars
 
# Atau enkripsi file plaintext yang sudah ada
sops --encrypt --in-place terraform.tfvars
 
# Dekripsi tanpa menulis ke disk, lalu pipe ke Terraform
sops --decrypt terraform.tfvars | terraform plan -var-file=-
sops --decrypt terraform.tfvars | terraform apply -var-file=-

Tip

Nilai SOPS ada pada repositori berisi file terenkripsi yang bisa di-review dan di-audit, tanpa memerlukan layanan secret store tambahan. Cocok untuk tim kecil atau lingkungan yang belum memiliki infrastruktur Vault. Kunci age/pgp/KMS itu sendiri tidak boleh berada di repo — letakkan di CI/CD secret atau ~/.config/sops/age/ di lokal dengan chmod 600.

Perbandingan Sumber Secret

Mari bandingkan ketiga pendekatan di atas dalam satu pandangan:

provider "vault" {
  address = "https://vault.internal.example.com:8200"
}
 
data "vault_kv_secret_v2" "db" {
  mount = "secret"
  name  = "production/database"
}
 
resource "aws_db_instance" "app" {
  username = data.vault_kv_secret_v2.db.data["username"]
  password = data.vault_kv_secret_v2.db.data["password"]
}
Sumber SecretIntegrasi di TerraformManaged Infra?Rotasi OtomatisCocok Untuk
HashiCorp VaultData source vault_kv_secret_v2Perlu infra sendiriYa (dynamic secret)Multi-cloud, workload identity, kebutuhan enterprise
AWS Secrets ManagerData source aws_secretsmanager_secret_versionAWSYa (via Lambda)Semua resource AWS, tim tanpa infra tambahan
SOPSFile .tfvars terenkripsi di GitTidak perluManual (re-encrypt)Repo-centric, tim kecil, tanpa secret store

Menandai Rahasia dengan sensitive = true

Meskipun sensitive = true bukan perlindungan terhadap state, ia tetap wajib digunakan. Fungsinya: mencegah nilai rahasia tampil di output terminal, log CI/CD, dan artifact plan. Mari lihat praktiknya.

Pertama, tandai variabel yang berisi secret:

variables.tf
variable "db_username" {
  type        = string
  description = "Username koneksi database."
  sensitive   = true
}
 
variable "db_password" {
  type        = string
  description = "Password koneksi database."
  sensitive   = true
}
 
variable "db_host" {
  type        = string
  description = "Alamat host database (bukan rahasia)."
  default     = "localhost"
}

Kedua, tandai output yang sensitif:

outputs.tf
output "db_endpoint" {
  description = "Endpoint database production."
  value       = aws_db_instance.app.endpoint
  sensitive   = true
}

Saat terraform apply dijalankan, nilai ini disembunyikan dari terminal:

Output apply dengan sensitive value
Apply complete! Resources: 5 added, 0 changed, 0 destroyed.
 
Outputs:
 
db_endpoint = <sensitive>

Sekalipun tidak terlihat, nilainya tetap tersimpan di state dan bisa dibaca dengan terraform output -json oleh mereka yang punya akses. Jadi ingat: sensitive = true adalah layar pengaman visual, bukan enkripsi.

Caution

Hati-hati dengan terraform plan -out=tfplan. File plan berformat biner menyimpan nilai sensitif, dan terraform show tfplan akan menampilkannya — termasuk nilai yang ditandai sensitive. Jangan pernah meng-commit file tfplan, dan pastikan file ini hanya ada di runner CI/CD yang aman (biasanya sudah di-.gitignore).

Kesalahan Umum dalam Secret Management & State Security

KesalahanGejalaSolusi
State ter-commit ke GitPassword plaintext bocor ke riwayat commit.gitignore + anggap semua secret bocor → rotasi total + bersihkan history (git filter-repo)
Hardcode secret di default variabelSecret terlihat di kode yang di-review semua orangPindahkan ke Vault/Secrets Manager/SOPS
.tfvars ikut ter-commitKredensial environment bocor.gitignore *.tfvars + rotasi
Backend tanpa KMS / encrypt=falseState tersimpan polos di S3Tambahkan SSE-KMS + encrypt = true
Bucket state tanpa versioningState corrupt tidak bisa di-rollbackAktifkan versioning + pertimbangkan backup berkala
IAM terlalu terbuka ke bucket stateSemua engineer bisa membaca secret dari stateLeast privilege + MFA + batasi ke runner CI
Satu secret store untuk semua envDeveloper bisa membaca secret produksiPisahkan path/secret per environment (dev/, prod/)
Meng-commit tfplanSecret di dalam plan bocorJangan commit plan; jaga di runner CI saja

Penutup

Pada episode 15 ini kita telah belajar bahwa state file adalah gudang secret dalam plaintext — ia menyimpan password, kredensial, dan private key apa adanya. Kita juga sudah melihat cara mengamankannya berlapis: enkripsi at-rest dengan KMS, versioning, block public access, dan IAM strict yang menerapkan least privilege — karena siapa pun yang bisa membaca state berarti bisa membaca seluruh secret produksi. Terakhir, kita membahas integrasi secret dari HashiCorp Vault, AWS Secrets Manager, dan SOPS, serta menandai variabel dan output dengan sensitive = true agar nilai rahasia tidak muncul di terminal dan log.

Poin kunci yang perlu kalian bawa pulang:

  • sensitive = true hanya menyamarkan tampilan, bukan enkripsi — state tetap menyimpan nilai asli.
  • Enkripsi at-rest + versioning + IAM strict adalah fondasi keamanan state.
  • Jangan pernah hardcode secret; ambil dari secret store atau file terenkripsi.
  • .gitignore untuk *.tfvars dan *.tfstate* wajib dibuat sejak hari pertama.
  • File plan (tfplan) sama sensitifnya dengan state.

Sekarang secret sudah tersimpan rapi dan state aman. Tapi bagaimana kalau ada orang lain di tim yang membuka port SSH ke publik, atau membuat bucket publik di environment produksi? Persetujuan manual saja tidak akan cukup.

Di episode 16 selanjutnya kita akan membahas Policy as Code (PaC) dengan OPA & Sentinel — cara otomatis menghentikan terraform apply jika kode melanggar aturan keamanan atau standar biaya organisasi. Pastikan tetap semangat!

Belajar Terraform - Secret Management & State Security | Belajar Terraform