TDCTF Academy Logo TDCTF ACADEMY

Terraform Security

Pendahuluan

Terraform adalah Infrastructure as Code (IaC) tool paling populer yang dikembangkan oleh HashiCorp. Dengan Terraform, infrastruktur cloud - server, database, jaringan, load balancer - didefinisikan sebagai kode dan dikelola melalui siklus hidup yang terotomatisasi.

Namun, kekuatan Terraform juga membawa risiko keamanan yang signifikan. State file Terraform berisi semua informasi infrastruktur termasuk nilai sensitif. Provider dan module dari sumber tidak terpercaya dapat menyuntikkan kode berbahaya. Kebijakan keamanan yang longgar dapat menyebabkan konfigurasi infrastruktur yang tidak aman.

Artikel ini membahas aspek keamanan kritis Terraform: keamanan state file, penanganan sensitive values, keamanan provider dan module, serta policy-as-code dengan Sentinel.

State File Security

State file (terraform.tfstate) adalah file JSON yang berisi representasi infrastruktur yang dikelola Terraform. File ini sangat sensitif karena berisi:

  • Resource identifiers: ARN, ID resource, IP addresses.
  • Sensitive values: Database passwords, API keys, private keys (meskipun ditandai sensitive).
  • Network topology: VPC IDs, subnet CIDRs, security group rules.

Remote State dengan S3 Backend

Menyimpan state file secara lokal sangat berbahaya. Praktik terbaik adalah menggunakan remote backend dengan enkripsi.

Contoh konfigurasi S3 backend:

terraform {
backend "s3" {
bucket = "company-terraform-state"
key = "production/network/terraform.tfstate"
region = "ap-southeast-1"
encrypt = true
dynamodb_table = "terraform-state-lock"
kms_key_id = "arn:aws:kms:ap-southeast-1:123456789012:key/abc123"
}
}

S3 Bucket untuk State File

Bucket S3 yang menyimpan state file harus dikonfigurasi dengan keamanan maksimal:

resource "aws_s3_bucket" "terraform_state" {
bucket = "company-terraform-state-123456789012"
}

resource "aws_s3_bucket_versioning" "terraform_state" {
bucket = aws_s3_bucket.terraform_state.id
versioning_configuration {
status = "Enabled"
}
}

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"
}
}
}

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_s3_bucket_policy" "terraform_state" {
bucket = aws_s3_bucket.terraform_state.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Principal = {
AWS = "arn:aws:iam::123456789012:role/terraform-backend-role"
}
Action = [
"s3:GetObject",
"s3:PutObject",
"s3:DeleteObject"
]
Resource = "${aws_s3_bucket.terraform_state.arn}/*"
Condition = {
Bool = {
"aws:SecureTransport": "true"
}
}
},
{
Effect = "Deny"
Principal = "*"
Action = "s3:*"
Resource = [
aws_s3_bucket.terraform_state.arn,
"${aws_s3_bucket.terraform_state.arn}/*"
]
Condition = {
Bool = {
"aws:SecureTransport": "false"
}
}
}
]
})
}

DynamoDB State Locking

DynamoDB digunakan untuk state locking - mencegah dua orang atau proses mengubah state secara bersamaan. Konfigurasi ini wajib untuk lingkungan tim.

resource "aws_dynamodb_table" "terraform_state_lock" {
name = "terraform-state-lock"
billing_mode = "PAY_PER_REQUEST"
hash_key = "LockID"
attribute {
name = "LockID"
type = "S"
}
server_side_encryption {
enabled = true
}
point_in_time_recovery {
enabled = true
}
}

Dengan konfigurasi di atas:

  • State file dienkripsi dengan KMS (AWS KMS).
  • Akses S3 dibatasi hanya untuk role tertentu.
  • Semua akses melalui HTTPS (SecureTransport).
  • State locking menggunakan DynamoDB.
  • Versioning diaktifkan untuk pemulihan.

Alternatif Backend: Terraform Cloud / HCP Terraform

HashiCorp Cloud Platform (HCP) Terraform menyediakan backend terkelola dengan fitur keamanan tambahan:

terraform {
cloud {
organization = "example-org"
workspaces {
name = "production-infrastructure"
}
}
}

Keuntungan HCP Terraform:

  • Enkripsi state at-rest dan in-transit.
  • Audit logging.
  • Role-based access control (RBAC).
  • Run tasks untuk policy enforcement (Sentinel).
  • Private registry untuk module.

Sensitive Values

Terraform memiliki mekanisme untuk melindungi nilai sensitif seperti password dan API keys.

Parameter sensitive = true

resource "aws_db_instance" "database" {
username = "admin"
password = random_password.db_password.result
# password tidak akan muncul di output terraform plan/apply
}

resource "random_password" "db_password" {
length = 24
special = true
override_special = "!#$%&*()-_=+[]:."
}

output "db_password" {
value = random_password.db_password.result
sensitive = true # tidak ditampilkan di output
}

output "db_endpoint" {
value = aws_db_instance.database.endpoint
# tidak sensitive - endpoint boleh ditampilkan
}

Dengan sensitive = true, nilai tersebut:

  • Tidak akan muncul di output terraform plan atau terraform apply.
  • Tidak akan disimpan dalam log.
  • Akan tetap tersimpan di state file (terenkripsi jika backend mendukung).
# Output nilai non-sensitive
terraform output db_endpoint

# Output nilai sensitive - harus dengan flag -json
terraform output -json db_password

# Simpan ke file dengan permission terbatas
terraform output -json > /tmp/output.json && chmod 600 /tmp/output.json

Input Variables dengan Sensitive Flag

variable "db_password" {
description = "Database administrator password"
type = string
sensitive = true
}

variable "api_keys" {
description = "Map of API keys per service"
type = map(string)
sensitive = true
}

Menggunakan Vault untuk Secrets Management

Untuk manajemen secrets yang lebih baik, integrasikan Terraform dengan Vault:

# Gunakan Vault provider
provider "vault" {
address = "https://vault.example.com:8200"
token = var.vault_token
}

# Ambil secret dari Vault
data "vault_generic_secret" "db_credentials" {
path = "secret/data/database/production"
}

resource "aws_db_instance" "database" {
username = data.vault_generic_secret.db_credentials.data["username"]
password = data.vault_generic_secret.db_credentials.data["password"]
# Secret tidak disimpan di state file Terraform - hanya referensi
}

Provider Security

Provider Terraform adalah plugin yang berkomunikasi dengan API penyedia infrastruktur. Keamanan provider sangat penting karena provider memiliki akses luas ke infrastruktur.

Provider Version Pinning

Selalu pin versi provider untuk menghindari perubahan yang tidak terduga:

terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0" # kompatibel dengan 5.x
}
random = {
source = "hashicorp/random"
version = "3.6.0"
}
}
}

Verification Provider Signature

Provider dari HashiCorp Registry telah ditandatangani. Terraform secara otomatis memverifikasi signature provider saat inisialisasi:

# Verifikasi dilakukan otomatis oleh terraform init
# HashiCorp Registry menggunakan GnuPG signature
terraform init

Untuk self-signed provider atau provider dari registry khusus, atur verifikasi:

terraform {
required_providers {
custom = {
source = "example.com/org/custom"
version = "1.0.0"
}
}
}

Provider Credentials

Jangan pernah menyimpan kredensial provider di kode. Gunakan environment variables, Vault, atau OIDC:

# BURUK - hardcoded credentials
provider "aws" {
region = "ap-southeast-1"
access_key = "AKIAIOSFODNN7EXAMPLE"
secret_key = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
}

# BAIK - gunakan environment variables atau credential chain
provider "aws" {
region = "ap-southeast-1"
# Credential diambil dari AWS_DEFAULT_REGION, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY
# Atau dari ~/.aws/credentials
# Atau dari instance profile (EC2)
}

# BAIK - OIDC untuk CI/CD
provider "aws" {
region = "ap-southeast-1"
assume_role {
role_arn = "arn:aws:iam::123456789012:role/terraform-role"
session_name = "terraform-ci"
}
}

Module Sourcing

Module Terraform memungkinkan penggunaan ulang kode infrastruktur. Namun, modul dari sumber yang tidak terpercaya dapat mengandung kode berbahaya.

Pinned Module Versions

# BURUK - versi tidak ditentukan
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
}

# BAIK - versi di-pin
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "5.1.2"
}

# LEBIH BAIK - pin ke commit spesifik
module "vpc" {
source = "github.com/terraform-aws-modules/terraform-aws-vpc?ref=e21d5f8e8c5c4b3a2a1f0d9e8c7b6a5f4e3d2c1"
}

Module Registry Verification

Gunakan modul hanya dari Terraform Registry yang terverifikasi:

# Terverifikasi - ditandai dengan "Verified" badge di registry
module "ec2" {
source = "terraform-aws-modules/ec2-instance/aws"
version = "5.5.0"
name = "web-server"
}

Untuk module internal, gunakan private registry:

# Module dari private registry HCP Terraform
module "internal-base" {
source = "app.terraform.io/example/internal-base/aws"
version = "1.2.0"
}

Audit Module Content

Sebelum menggunakan module baru:

  1. Audit source code module secara manual.
  2. Periksa apakah module membutuhkan akses yang tidak semestinya.
  3. Verifikasi bahwa module tidak mengirim data ke pihak ketiga.
  4. Gunakan terraform plan untuk melihat perubahan yang akan dilakukan module.

Sentinel Policies (HashiCorp Cloud Platform)

Sentinel adalah policy-as-code framework dari HashiCorp yang memungkinkan Anda mendefinisikan kebijakan keamanan yang dijalankan sebelum terraform apply.

Contoh Sentinel Policy

Kebijakan: Larang security group dengan port 22 (SSH) terbuka ke 0.0.0.0/0:

# deny-public-ssh.sentinel
import "tfplan"

# Cari semua resource aws_security_group_rule
ssh_rules = filter tfplan.resource_changes as _, rc in [
rc.mode is "managed" and
rc.type is "aws_security_group_rule"
]

# Periksa apakah ada aturan SSH yang terbuka ke publik
violations = []
for rule in ssh_rules {
if rule.change.after.type is "ingress" and
rule.change.after.from_port <= 22 and
rule.change.after.to_port >= 22 and
rule.change.after.cidr_blocks contains "0.0.0.0/0" {
violations = append(violations, rule.change.after)
}
}

# Jika ada pelanggaran, reject
if length(violations) > 0 {
print("Found", length(violations), "SSH rules open to 0.0.0.0/0:")
for violation in violations {
print(" -", violation.cidr_blocks, "port 22")
}
}
else {
main = true
}

Kebijakan: Wajib Enkripsi S3 Bucket

# enforce-s3-encryption.sentinel
import "tfplan"

# Filter resource S3 bucket
s3_buckets = filter tfplan.resource_changes as _, rc in [
rc.mode is "managed" and
rc.type is "aws_s3_bucket"
]

# Periksa enkripsi
violations = []
for bucket in s3_buckets {
encryption = bucket.change.after.server_side_encryption_configuration
if encryption is undefined or encryption.rule is undefined {
violations = append(violations, bucket.change.after.bucket)
}
}

# Enforce
main = length(violations) is 0

Integrasi Sentinel di Pipeline

Sentinel dijalankan sebagai run task di HCP Terraform atau Terraform Enterprise:

# GitHub Actions - integrasi dengan HCP Terraform
jobs:
terraform:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Terraform
uses: hashicorp/setup-terraform@v2
with:
cli_config_credentials_token: ${{ secrets.TF_API_TOKEN }}
- name: Terraform Plan
run: terraform plan
- name: Terraform Apply
# Policy check otomatis dijalankan oleh HCP Terraform
run: terraform apply -auto-approve

Sentinel policies dapat dikonfigurasi dengan tiga mode:

  • Advisory: Policy dilanggar tetapi tidak memblokir apply.
  • Soft mandatory: Policy dilanggar, perlu persetujuan admin.
  • Hard mandatory: Policy memblokir apply secara absolut.

Contoh: S3 Backend Lengkap dengan Keamanan

Berikut adalah konfigurasi lengkap backend Terraform yang aman:

# backend-config.hcl - konfigurasi untuk init
bucket = "company-tf-state-prod"
key = "terraform.tfstate"
region = "ap-southeast-1"
encrypt = true
kms_key_id = "arn:aws:kms:ap-southeast-1:123456789012:key/abc123"
dynamodb_table = "tf-state-locks"
profile = "terraform-admin"

Inisialisasi dengan konfigurasi backend:

terraform init -backend-config=backend-config.hcl

Konfigurasi Terraform untuk production:

# main.tf
terraform {
required_version = ">= 1.6.0"

required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.40"
}
random = {
source = "hashicorp/random"
version = "~> 3.6"
}
}

backend "s3" {
# Nilai diisi saat init via backend-config.hcl
}
}

provider "aws" {
region = var.aws_region
default_tags {
tags = {
Environment = var.environment
Terraform = "true"
ManagedBy = "devsecops-team"
}
}
}

# Variable definitions
variable "environment" {
description = "Environment name (production, staging, development)"
type = string
}

variable "aws_region" {
description = "AWS region"
type = string
default = "ap-southeast-1"
}

variable "vpc_cidr" {
description = "VPC CIDR block"
type = string
sensitive = false
}

# Remote state dari infrastruktur bersama
data "terraform_remote_state" "networking" {
backend = "s3"
config = {
bucket = "company-tf-state-shared"
key = "networking/terraform.tfstate"
region = "ap-southeast-1"
encrypt = true
}
}

# Module dari registry terverifikasi
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "5.8.1"

name = "${var.environment}-vpc"
cidr = var.vpc_cidr

azs = ["${var.aws_region}a", "${var.aws_region}b"]
private_subnets = ["10.0.1.0/24", "10.0.2.0/24"]
public_subnets = ["10.0.101.0/24", "10.0.102.0/24"]

enable_nat_gateway = true
enable_vpn_gateway = false

tags = {
Environment = var.environment
}
}

# Resource dengan sensitive values
resource "random_password" "db_master" {
length = 24
special = false
}

resource "aws_db_instance" "database" {
identifier = "${var.environment}-database"

engine = "postgres"
engine_version = "16.3"
instance_class = "db.t3.medium"

db_name = "appdb"
username = "appadmin"
password = random_password.db_master.result

vpc_security_group_ids = [aws_security_group.database.id]
db_subnet_group_name = aws_db_subnet_group.database.name

storage_encrypted = true
storage_type = "gp3"
allocated_storage = 100

backup_retention_period = 35
backup_window = "03:00-04:00"
maintenance_window = "sun:04:00-sun:05:00"

deletion_protection = true
skip_final_snapshot = false
final_snapshot_identifier = "${var.environment}-database-final"

enabled_cloudwatch_logs_exports = ["postgresql"]
}

# Output aman
output "db_endpoint" {
value = aws_db_instance.database.endpoint
}

output "db_port" {
value = aws_db_instance.database.port
}

output "db_name" {
value = aws_db_instance.database.db_name
}

output "db_username" {
value = aws_db_instance.database.username
}

output "db_password" {
value = random_password.db_master.result
sensitive = true
}

Kesimpulan

Keamanan Terraform membutuhkan perhatian di setiap lapisan: state file harus diamankan dengan remote backend terenkripsi dan state locking; sensitive values harus dilindungi dengan flag sensitive dan integrasi Vault; provider dan module harus diverifikasi dan versinya di-pin; dan kebijakan keamanan dapat ditegakkan dengan Sentinel policies. Dengan menerapkan praktik-praktik ini, Anda dapat menggunakan Terraform secara aman untuk mengelola infrastruktur skala besar.

PADA HALAMAN INI