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 revertile 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.
