Terraform ile Güvenlik Grubu ve Firewall Kuralları Yönetimi

Altyapı güvenliği söz konusu olduğunda, güvenlik grupları ve firewall kuralları her şeyin temelini oluşturuyor. Elle yapılan konfigürasyonlar zamanla kaçınılmaz olarak tutarsızlaşıyor, dökümantasyon güncel kalmıyor ve “bu portu kim açtı, neden açtı” soruları cevapsız kalıyor. Terraform ile bu kuralları kod olarak yönetmek, hem audit trail sağlıyor hem de ekip içi tutarlılığı garantiliyor. Bu yazıda AWS güvenlik grupları ve çeşitli firewall senaryoları üzerinden gerçek dünya örneklerine bakacağız.

Neden Güvenlik Kurallarını Kod Olarak Yönetmeliyiz

Klasik senaryoyu hepimiz yaşadık: Prodüksiyon ortamında bir sorun çıkıyor, biri aceleyle AWS Console’a girip “geçici olarak” 0.0.0.0/0 üzerinden bir port açıyor. Sorun çözülüyor, port açık kalıyor. Üç ay sonra güvenlik taraması yapıldığında bu kural hala orada duruyor ve kimse neden açıldığını hatırlamıyor.

Terraform ile güvenlik kurallarını yönetirken her değişiklik Git geçmişinde görünüyor. Kim ne zaman hangi kuralı ekledi, neden ekledi, PR’da ne konuşuldu, bunların hepsine ulaşabiliyorsunuz. Üstelik terraform plan çıktısı sayesinde bir kuralı silmeden önce ne etki yaratacağını görebiliyorsunuz.

AWS Güvenlik Grubu Temel Yapısı

AWS üzerinde çalışıyorsak güvenlik grupları hem inbound hem outbound trafiği kontrol eden stateful bir yapı sunuyor. Terraform’da aws_security_group resource’u ile bu yapıyı tanımlıyoruz.

# Temel bir web sunucusu güvenlik grubu
resource "aws_security_group" "web_server" {
  name        = "web-server-sg"
  description = "Web sunucusu için HTTP/HTTPS güvenlik grubu"
  vpc_id      = aws_vpc.main.id

  # HTTP trafiği
  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
    description = "HTTP erişimi herkese açık"
  }

  # HTTPS trafiği
  ingress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
    description = "HTTPS erişimi herkese açık"
  }

  # SSH sadece VPN CIDR'ından
  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["10.0.0.0/8"]
    description = "SSH sadece iç ağdan"
  }

  # Tüm outbound trafiğe izin ver
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
    description = "Tüm outbound trafiğe izin"
  }

  tags = {
    Name        = "web-server-sg"
    Environment = var.environment
    ManagedBy   = "terraform"
  }
}

Burada dikkat etmemiz gereken bazı noktalar var. protocol = "-1" kullanımı tüm protokollere izin vermek için kullanılıyor ve genellikle egress kurallarında tercih ediliyor. from_port ve to_port değerlerini aynı yaparsanız tek port, farklı yaparsanız port aralığı tanımlamış oluyorsunuz.

Güvenlik Grubu Kurallarını Ayrı Yönetmek

Inline ingress/egress tanımlamak yerine aws_security_group_rule resource’unu kullanmak, özellikle modüler yapılarda çok daha fazla esneklik sağlıyor. Bu yaklaşım cycle dependency sorunlarını da ortadan kaldırıyor.

# Güvenlik grubunu boş oluştur
resource "aws_security_group" "app_tier" {
  name        = "${var.project}-${var.environment}-app-sg"
  description = "Uygulama katmanı güvenlik grubu"
  vpc_id      = var.vpc_id

  lifecycle {
    create_before_destroy = true
  }

  tags = local.common_tags
}

# Kuralları ayrı resource'larla ekle
resource "aws_security_group_rule" "app_ingress_from_alb" {
  type                     = "ingress"
  from_port                = 8080
  to_port                  = 8080
  protocol                 = "tcp"
  source_security_group_id = aws_security_group.alb.id
  security_group_id        = aws_security_group.app_tier.id
  description              = "ALB'den uygulama portuna erişim"
}

resource "aws_security_group_rule" "app_egress_to_db" {
  type                     = "egress"
  from_port                = 5432
  to_port                  = 5432
  protocol                 = "tcp"
  source_security_group_id = aws_security_group.database.id
  security_group_id        = aws_security_group.app_tier.id
  description              = "Veritabanına PostgreSQL bağlantısı"
}

resource "aws_security_group_rule" "app_egress_https" {
  type              = "egress"
  from_port         = 443
  to_port           = 443
  protocol          = "tcp"
  cidr_blocks       = ["0.0.0.0/0"]
  security_group_id = aws_security_group.app_tier.id
  description       = "Dış servislere HTTPS çağrıları"
}

create_before_destroy: Güvenlik grubunu güncellerken önce yeni grubu oluştur, sonra eskisini sil. Bu sayede kısa süreli de olsa güvenlik açığı oluşmuyor.

Dinamik Kural Tanımları ile Ölçeklenebilirlik

Onlarca kuralı tek tek yazmak hem yorucu hem de hata yaratma ihtimali yüksek bir süreç. Terraform’un dynamic bloğu bu noktada hayat kurtarıyor.

# variables.tf
variable "allowed_ingress_rules" {
  description = "İzin verilen inbound kuralları listesi"
  type = list(object({
    from_port   = number
    to_port     = number
    protocol    = string
    cidr_blocks = list(string)
    description = string
  }))
  default = [
    {
      from_port   = 80
      to_port     = 80
      protocol    = "tcp"
      cidr_blocks = ["0.0.0.0/0"]
      description = "HTTP"
    },
    {
      from_port   = 443
      to_port     = 443
      protocol    = "tcp"
      cidr_blocks = ["0.0.0.0/0"]
      description = "HTTPS"
    },
    {
      from_port   = 8443
      to_port     = 8443
      protocol    = "tcp"
      cidr_blocks = ["10.0.0.0/8", "172.16.0.0/12"]
      description = "Admin panel iç ağdan"
    }
  ]
}

# main.tf
resource "aws_security_group" "dynamic_sg" {
  name   = "dynamic-rules-sg"
  vpc_id = var.vpc_id

  dynamic "ingress" {
    for_each = var.allowed_ingress_rules
    content {
      from_port   = ingress.value.from_port
      to_port     = ingress.value.to_port
      protocol    = ingress.value.protocol
      cidr_blocks = ingress.value.cidr_blocks
      description = ingress.value.description
    }
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

Bu yapıyı kullandığınızda yeni bir kural eklemek için sadece allowed_ingress_rules listesine yeni bir obje eklemeniz yetiyor. CI/CD pipeline’ınız otomatik olarak terraform plan çalıştırıp değişikliği gösteriyor.

Çok Katmanlı Mimari Güvenlik Grubu Yapısı

Gerçek dünya uygulamalarında genellikle üç katmanlı bir mimari görüyoruz: Load Balancer, Application, Database. Bu katmanlar arasındaki iletişimi güvenlik gruplarıyla nasıl kontrol edeceğimize bakalım.

# ALB Güvenlik Grubu - Internete açık
resource "aws_security_group" "alb" {
  name        = "${local.prefix}-alb-sg"
  description = "Application Load Balancer güvenlik grubu"
  vpc_id      = aws_vpc.main.id

  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
    description = "HTTP"
  }

  ingress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
    description = "HTTPS"
  }

  egress {
    from_port       = 8080
    to_port         = 8080
    protocol        = "tcp"
    security_groups = [aws_security_group.app.id]
    description     = "Uygulama sunucularına trafik"
  }
}

# Uygulama Katmanı - Sadece ALB'den trafik alır
resource "aws_security_group" "app" {
  name        = "${local.prefix}-app-sg"
  description = "Uygulama sunucuları güvenlik grubu"
  vpc_id      = aws_vpc.main.id

  ingress {
    from_port       = 8080
    to_port         = 8080
    protocol        = "tcp"
    security_groups = [aws_security_group.alb.id]
    description     = "ALB'den gelen trafik"
  }

  egress {
    from_port       = 5432
    to_port         = 5432
    protocol        = "tcp"
    security_groups = [aws_security_group.database.id]
    description     = "Veritabanı bağlantısı"
  }

  egress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
    description = "Dış API çağrıları"
  }
}

# Veritabanı Katmanı - Sadece App katmanından erişim
resource "aws_security_group" "database" {
  name        = "${local.prefix}-db-sg"
  description = "RDS veritabanı güvenlik grubu"
  vpc_id      = aws_vpc.main.id

  ingress {
    from_port       = 5432
    to_port         = 5432
    protocol        = "tcp"
    security_groups = [aws_security_group.app.id]
    description     = "Sadece uygulama katmanından PostgreSQL"
  }

  # Veritabanı outbound trafiğe ihtiyaç duymaz
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["127.0.0.1/32"]
    description = "Outbound trafiği engelle"
  }
}

Bu yapının güzelliği şu: veritabanına doğrudan internetten ya da ALB’den erişim mümkün değil. Trafik mutlaka ALB, uygulama sunucusu, veritabanı zincirini takip etmek zorunda.

AWS Network ACL ile Ek Güvenlik Katmanı

Güvenlik grupları instance seviyesinde çalışırken Network ACL’ler subnet seviyesinde çalışıyor. Bir sysadmin olarak her ikisini de kullanmak katmanlı güvenlik sağlıyor.

resource "aws_network_acl" "private_subnet" {
  vpc_id     = aws_vpc.main.id
  subnet_ids = aws_subnet.private[*].id

  # Kötü niyetli IP bloğunu engelle
  ingress {
    rule_no    = 50
    action     = "deny"
    protocol   = "tcp"
    from_port  = 0
    to_port    = 65535
    cidr_block = "192.0.2.0/24"
  }

  # VPC içinden gelen trafiğe izin ver
  ingress {
    rule_no    = 100
    action     = "allow"
    protocol   = "tcp"
    from_port  = 0
    to_port    = 65535
    cidr_block = var.vpc_cidr
  }

  # Ephemeral portlar - response trafiği için
  ingress {
    rule_no    = 200
    action     = "allow"
    protocol   = "tcp"
    from_port  = 1024
    to_port    = 65535
    cidr_block = "0.0.0.0/0"
  }

  # Tüm outbound
  egress {
    rule_no    = 100
    action     = "allow"
    protocol   = "-1"
    from_port  = 0
    to_port    = 0
    cidr_block = "0.0.0.0/0"
  }

  tags = merge(local.common_tags, {
    Name = "${local.prefix}-private-nacl"
  })
}

Network ACL’lerde kural sırası kritik. Düşük numara önce değerlendiriliyor ve deny kuralı allow kuralından önce gelmelidir.

GCP ve Azure Firewall Kuralları

Sadece AWS değil, diğer cloud providerları da Terraform ile aynı kolaylıkta yönetilebiliyor.

# GCP Firewall Kuralı
resource "google_compute_firewall" "allow_http" {
  name    = "${var.project}-allow-http"
  network = google_compute_network.main.name

  allow {
    protocol = "tcp"
    ports    = ["80", "443", "8080-8090"]
  }

  source_ranges = ["0.0.0.0/0"]
  target_tags   = ["web-server"]

  description = "Web sunucularına HTTP/HTTPS erişimi"
}

resource "google_compute_firewall" "allow_internal" {
  name    = "${var.project}-allow-internal"
  network = google_compute_network.main.name

  allow {
    protocol = "tcp"
    ports    = ["0-65535"]
  }

  allow {
    protocol = "udp"
    ports    = ["0-65535"]
  }

  allow {
    protocol = "icmp"
  }

  source_ranges = [var.internal_cidr]
  description   = "İç ağ trafiği"
}

# Azure Network Security Group
resource "azurerm_network_security_group" "web" {
  name                = "${var.project}-web-nsg"
  location            = azurerm_resource_group.main.location
  resource_group_name = azurerm_resource_group.main.name

  security_rule {
    name                       = "allow-https"
    priority                   = 100
    direction                  = "Inbound"
    access                     = "Allow"
    protocol                   = "Tcp"
    source_port_range          = "*"
    destination_port_range     = "443"
    source_address_prefix      = "*"
    destination_address_prefix = "*"
    description                = "HTTPS erişimi"
  }

  security_rule {
    name                       = "deny-all-inbound"
    priority                   = 4096
    direction                  = "Inbound"
    access                     = "Deny"
    protocol                   = "*"
    source_port_range          = "*"
    destination_port_range     = "*"
    source_address_prefix      = "*"
    destination_address_prefix = "*"
    description                = "Varsayılan tüm girişi engelle"
  }
}

Güvenlik Konfigürasyonunu Değişkenler ile Ortama Göre Ayarlamak

Geliştirme ve prodüksiyon ortamları için farklı güvenlik kuralları genellikle bir gereksinim. Terraform’da bunu şöyle yönetebilirsiniz:

# locals.tf
locals {
  # Ortama göre SSH erişim CIDR'ları
  ssh_allowed_cidrs = {
    development = ["0.0.0.0/0"]  # Dev ortamda esnek
    staging     = ["10.0.0.0/8", "172.16.0.0/12"]
    production  = ["10.50.0.0/24"]  # Sadece VPN CIDR'ı
  }

  # Ortama göre izleme portları
  monitoring_ports = {
    development = [9090, 9100, 3000, 8080]
    staging     = [9090, 9100]
    production  = [9090, 9100]
  }

  current_ssh_cidrs      = local.ssh_allowed_cidrs[var.environment]
  current_monitoring     = local.monitoring_ports[var.environment]
}

resource "aws_security_group_rule" "ssh_access" {
  type              = "ingress"
  from_port         = 22
  to_port           = 22
  protocol          = "tcp"
  cidr_blocks       = local.current_ssh_cidrs
  security_group_id = aws_security_group.bastion.id
  description       = "SSH erişimi - ${var.environment} ortamı"
}

resource "aws_security_group_rule" "monitoring" {
  count = length(local.current_monitoring)

  type              = "ingress"
  from_port         = local.current_monitoring[count.index]
  to_port           = local.current_monitoring[count.index]
  protocol          = "tcp"
  cidr_blocks       = [var.monitoring_cidr]
  security_group_id = aws_security_group.app.id
  description       = "İzleme portu ${local.current_monitoring[count.index]}"
}

Terragrunt ile Çoklu Hesap Güvenlik Yönetimi

Birden fazla AWS hesabı yönetiyorsanız, Terragrunt güvenlik kurallarını tutarlı tutmak için güzel bir çözüm sunuyor. Ortak modülü bir kez yazıp her ortamda parametreleyebilirsiniz.

# modules/security-groups/main.tf modülü
variable "vpc_id" {}
variable "environment" {}
variable "allowed_ssh_cidrs" {
  type = list(string)
}
variable "app_port" {
  default = 8080
}

output "web_sg_id" {
  value = aws_security_group.web.id
}

output "app_sg_id" {
  value = aws_security_group.app.id
}

output "db_sg_id" {
  value = aws_security_group.database.id
}
# terragrunt.hcl - production hesabı
terraform {
  source = "git::https://github.com/sirket/terraform-modules.git//security-groups?ref=v2.1.0"
}

inputs = {
  vpc_id            = dependency.vpc.outputs.vpc_id
  environment       = "production"
  allowed_ssh_cidrs = ["10.50.0.0/24"]
  app_port          = 443
}

Güvenlik Konfigürasyonunu Test Etmek

Güvenlik kurallarını yazdıktan sonra doğrulama yapmak kritik. Terraform’la birlikte terratest ya da basit script’ler kullanabilirsiniz.

#!/bin/bash
# security_group_audit.sh
# Güvenlik grubu kurallarını denetleme scripti

SG_ID="$1"
REGION="${2:-eu-west-1}"

if [ -z "$SG_ID" ]; then
  echo "Kullanım: $0 <security-group-id> [region]"
  exit 1
fi

echo "=== Güvenlik Grubu Denetimi: $SG_ID ==="
echo ""

# Tüm dünyaya açık portları kontrol et
echo ">> 0.0.0.0/0 veya ::/0 erişimine açık kurallar:"
aws ec2 describe-security-groups 
  --group-ids "$SG_ID" 
  --region "$REGION" 
  --query 'SecurityGroups[0].IpPermissions[?IpRanges[?CidrIp==`0.0.0.0/0`]||Ipv6Ranges[?CidrIpv6==`::/0`]]' 
  --output table

echo ""
echo ">> SSH (port 22) erişim kontrolü:"
aws ec2 describe-security-groups 
  --group-ids "$SG_ID" 
  --region "$REGION" 
  --query 'SecurityGroups[0].IpPermissions[?FromPort==`22`]' 
  --output json

echo ""
echo ">> RDP (port 3389) erişim kontrolü:"
aws ec2 describe-security-groups 
  --group-ids "$SG_ID" 
  --region "$REGION" 
  --query 'SecurityGroups[0].IpPermissions[?FromPort==`3389`]' 
  --output json

echo ""
echo "Denetim tamamlandi."

Bu script’i CI/CD pipeline’ınıza ekleyerek her değişiklikten sonra otomatik güvenlik denetimi yapabilirsiniz.

Yaygın Hatalar ve Çözümleri

Terraform ile güvenlik grupları yönetirken sıkça karşılaşılan sorunlar ve çözümleri:

Inline vs Resource çakışması: Aynı güvenlik grubunu hem inline hem de aws_security_group_rule ile yönetmeye çalışırsanız Terraform sürekli değişiklik gösterir. Bir yaklaşımı seçip ona bağlı kalın.

Circular dependency: İki güvenlik grubu birbirinin kaynak grubu olarak tanımlanınca döngüsel bağımlılık oluşur. Bunu çözmek için güvenlik gruplarını önce boş oluşturun, sonra kuralları ayrı resource’larla ekleyin.

description alanını boş bırakmak: Teknik bir sorun olmasa da altı ay sonra kimin neden eklediğini anlamak imkansız hale geliyor. Her kurala açıklayıcı bir description eklemek sysadmin görevinin parçası.

Plan sırasında beklenmedik silmeler: Güvenlik grubunu kullanan kaynaklar (EC2, RDS, vb.) güncel Terraform state’inde yoksa Terraform grubu silmeye çalışabilir. lifecycle bloğunda prevent_destroy = true bunu engelliyor.

# Kritik güvenlik grupları için koruma
resource "aws_security_group" "production_db" {
  name   = "prod-database-sg"
  vpc_id = var.vpc_id

  lifecycle {
    prevent_destroy = true
    ignore_changes  = [description]
  }
}

prevent_destroy: Bu güvenlik grubunu terraform destroy ile silmeyi engeller, production ortamlarda mutlaka kullanın.

ignore_changes: Belirtilen alanlar dışarıdan değiştirildiğinde Terraform bunu revert etmez.

Sonuç

Güvenlik grupları ve firewall kurallarını Terraform ile yönetmek, başlangıçta ek iş gibi görünse de uzun vadede ciddi zaman ve güvenlik kazanımı sağlıyor. En önemli faydaları özetlemek gerekirse:

  • Değişiklik takibi: Her kural ekleme ve silme Git geçmişinde, PR’da tartışılmış, onaylanmış
  • Ortam tutarlılığı: Development, staging ve production arasında aynı yapı, sadece parametreler değişiyor
  • Ekip işbirliği: Başka biri de aynı altyapıyı anlayabiliyor, kod yazdığınızda dökümantasyon kendiliğinden oluşuyor
  • Hızlı recovery: Bir şeyler bozulduğunda git revert ile eski konfigürasyona anında dönebiliyorsunuz
  • Güvenlik denetimleri: Automated pipeline ile her değişiklik denetlenebiliyor

Pratik öneri olarak, eğer mevcut bir ortamı Terraform’a taşıyorsanız terraform import ile güvenlik gruplarını state’e ekleyip sonra kodunu yazın. terraform plan tamamen boş çıkana kadar kod ile gerçek durum uyumsuzluğunu giderin. Sonra yavaş yavaş iyileştirmeye başlayın. Mükemmel başlangıç yerine başlangıç yapmak, güvenlik grubunuzun hangi instance’ta kullanıldığını anlamaktan çok daha değerli.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir