Microservice Mimarisinde API Güvenliği

Microservice mimarisine geçiş yaptığında, güvenlik konusu bambaşka bir boyut kazanıyor. Monolitik uygulamalarda tek bir güvenlik duvarı arkasına saklanabilen her şey, artık onlarca, belki yüzlerce servisin birbirleriyle konuştuğu, her noktasının potansiyel bir saldırı yüzeyi haline geldiği bir ortama dönüşüyor. Bunu ilk yaşadığımda gerçekten şok olmuştum. “Şirket içi ağda zaten güvenliyiz” diye düşünmek, bu dünyada seni mahvedebilir.

Bu yazıda gerçek prodüksiyon ortamlarında karşılaştığım sorunlardan ve bu sorunlara uyguladığım çözümlerden bahsedeceğim. JWT’den API Gateway güvenliğine, servisler arası kimlik doğrulamadan rate limiting stratejilerine kadar her şeyi ele alacağız.

Neden Microservice Güvenliği Farklıdır?

Monolitik yapıda tüm bileşenler aynı process içinde çalıştığı için iç iletişim zaten güvenliydi. Microservice mimarisinde ise her servis bağımsız bir süreç, genellikle ayrı bir container, hatta ayrı bir sunucu üzerinde çalışıyor. Order Service ile Payment Service arasındaki her HTTP çağrısı potansiyel olarak dinlenebilir, manipüle edilebilir veya taklit edilebilir.

Birkaç temel tehdit vektörü var:

  • Servis taklit saldırıları: Bir saldırgan, güvenilir bir servis gibi davranarak sisteme istek atabilir
  • Man-in-the-middle: Servisler arası şifresiz iletişim dinlenebilir
  • Lateral movement: Bir servisi ele geçiren saldırgan diğer servislere de sızabilir
  • Token sızıntısı: Uzun ömürlü tokenlar ele geçirildiğinde uzun süre kullanılabilir
  • Broken object level authorization: Her servis kendi yetkilendirmesini düzgün yapmak zorunda

API Gateway: Tek Giriş Noktasının Gücü

API Gateway, microservice güvenliğinin ilk savunma hattıdır. Tüm dış trafiği tek noktadan geçirerek merkezi kimlik doğrulama, rate limiting ve loglama yapabilirsin. Ben production ortamında genellikle Kong veya Nginx tabanlı çözümler kullanıyorum.

İşte basit bir Nginx tabanlı API Gateway konfigürasyonu:

# /etc/nginx/conf.d/api-gateway.conf

upstream order_service {
    server order-service:8001;
    keepalive 32;
}

upstream payment_service {
    server payment-service:8002;
    keepalive 32;
}

server {
    listen 443 ssl http2;
    server_name api.sirketim.com;

    ssl_certificate /etc/ssl/certs/api.crt;
    ssl_certificate_key /etc/ssl/private/api.key;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
    ssl_prefer_server_ciphers off;

    # Security headers
    add_header Strict-Transport-Security "max-age=63072000" always;
    add_header X-Content-Type-Options nosniff;
    add_header X-Frame-Options DENY;
    add_header Content-Security-Policy "default-src 'none'";

    # Rate limiting zone kullanımı
    limit_req zone=api_limit burst=20 nodelay;

    location /api/v1/orders {
        # JWT doğrulama için auth_request kullanımı
        auth_request /auth/validate;
        auth_request_set $auth_user $upstream_http_x_user_id;

        proxy_pass http://order_service;
        proxy_set_header X-User-ID $auth_user;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }

    location /auth/validate {
        internal;
        proxy_pass http://auth-service:8000/validate;
        proxy_pass_request_body off;
        proxy_set_header Content-Length "";
        proxy_set_header X-Original-URI $request_uri;
    }
}

# Rate limiting zone tanımı - http bloğunda olmalı
# http {
#     limit_req_zone $binary_remote_addr zone=api_limit:10m rate=100r/m;
# }

Bu konfigürasyonda dikkat etmen gereken önemli nokta: her istek /auth/validate endpoint’ine giderek doğrulanıyor ve kullanıcı bilgisi header üzerinden downstream servise iletiliyor.

JWT ile Kimlik Doğrulama

JWT (JSON Web Token) microservice dünyasının en yaygın kimlik doğrulama mekanizması. Stateless yapısı sayesinde her servis token’ı bağımsız olarak doğrulayabilir, merkezi bir session store’a ihtiyaç kalmıyor. Ama JWT’yi yanlış kullanmak da çok kolay.

İşte production’da kullandığım JWT doğrulama scripti:

#!/bin/bash
# jwt-validate.sh - JWT token yapısını kontrol eden yardımcı script

TOKEN=$1

if [ -z "$TOKEN" ]; then
    echo "Kullanım: $0 <jwt_token>"
    exit 1
fi

# Token'ı parçalarına ayır
HEADER=$(echo $TOKEN | cut -d'.' -f1)
PAYLOAD=$(echo $TOKEN | cut -d'.' -f2)

# Base64 decode et (padding ekle)
decode_base64() {
    local input=$1
    local mod4=$((${#input} % 4))
    if [ $mod4 -eq 2 ]; then
        input="${input}=="
    elif [ $mod4 -eq 3 ]; then
        input="${input}="
    fi
    echo "$input" | tr '_-' '/+' | base64 -d 2>/dev/null
}

echo "=== JWT Token Analizi ==="
echo ""
echo "Header:"
decode_base64 "$HEADER" | python3 -m json.tool

echo ""
echo "Payload:"
DECODED_PAYLOAD=$(decode_base64 "$PAYLOAD")
echo $DECODED_PAYLOAD | python3 -m json.tool

# Token expiry kontrolü
EXP=$(echo $DECODED_PAYLOAD | python3 -c "import sys, json; d=json.load(sys.stdin); print(d.get('exp', 0))")
CURRENT_TIME=$(date +%s)

if [ "$EXP" -lt "$CURRENT_TIME" ]; then
    echo ""
    echo "UYARI: Token süresi dolmuş!"
    echo "Dolma zamanı: $(date -d @$EXP)"
else
    REMAINING=$((EXP - CURRENT_TIME))
    echo ""
    echo "Token geçerli. Kalan süre: $((REMAINING / 60)) dakika"
fi

JWT güvenliğinde en sık yapılan hatalar şunlar:

  • algorithm: none saldırısı: Kütüphane konfigürasyonunda alg=none kabul etmemeyi unutmak
  • Uzun token ömrü: Access token’ın ömrünü 15-30 dakikadan fazla tutmak
  • Hassas veri depolama: JWT payload şifresiz okunabilir, oraya şifre veya kredi kartı bilgisi koyma
  • İmza doğrulamayı atlamak: “Zaten gateway doğruladı” diyerek servislerde imza kontrolü yapmamak

mTLS ile Servisler Arası Güvenlik

“Zero trust” mimarisinin temel taşı olan mTLS (mutual TLS), her iki tarafın da sertifika ile kimliğini kanıtladığı bir mekanizma. Prodüksiyonda Kubernetes kullanıyorsan Istio veya Linkerd bu işi otomatik halleder. Ama bare metal veya basit Docker Swarm ortamında bunu manuel yapman gerekebilir.

#!/bin/bash
# mtls-cert-generator.sh
# Servisler arası mTLS için sertifika üretme

CA_DIR="/etc/microservices/ca"
CERTS_DIR="/etc/microservices/certs"

mkdir -p $CA_DIR $CERTS_DIR

# Root CA oluştur (bunu güvenli bir yerde sakla!)
generate_ca() {
    echo "Root CA oluşturuluyor..."
    openssl genrsa -aes256 -out $CA_DIR/ca.key 4096

    openssl req -new -x509 -days 3650 
        -key $CA_DIR/ca.key 
        -out $CA_DIR/ca.crt 
        -subj "/C=TR/ST=Istanbul/O=SirketimAS/CN=Microservices-Root-CA"

    echo "Root CA oluşturuldu: $CA_DIR/ca.crt"
}

# Servis sertifikası oluştur
generate_service_cert() {
    SERVICE_NAME=$1

    if [ -z "$SERVICE_NAME" ]; then
        echo "Servis adı gerekli!"
        exit 1
    fi

    SERVICE_DIR="$CERTS_DIR/$SERVICE_NAME"
    mkdir -p $SERVICE_DIR

    # Private key
    openssl genrsa -out $SERVICE_DIR/service.key 2048

    # CSR oluştur
    openssl req -new 
        -key $SERVICE_DIR/service.key 
        -out $SERVICE_DIR/service.csr 
        -subj "/C=TR/ST=Istanbul/O=SirketimAS/CN=$SERVICE_NAME.internal"

    # CA ile imzala
    openssl x509 -req -days 365 
        -in $SERVICE_DIR/service.csr 
        -CA $CA_DIR/ca.crt 
        -CAkey $CA_DIR/ca.key 
        -CAcreateserial 
        -out $SERVICE_DIR/service.crt 
        -extensions v3_req

    echo "$SERVICE_NAME servisi için sertifika oluşturuldu"
    echo "Cert: $SERVICE_DIR/service.crt"
    echo "Key: $SERVICE_DIR/service.key"
}

# Kullanım
case "$1" in
    "ca")
        generate_ca
        ;;
    "service")
        generate_service_cert $2
        ;;
    *)
        echo "Kullanım: $0 {ca|service <servis-adi>}"
        exit 1
        ;;
esac

Rate Limiting ve DDoS Koruması

Bir şirkette güzel bir hikaye yaşadım. Geliştirici ekibi yanlışlıkla sonsuz döngüye giren bir servis yazmış ve bu servis dakikada binlerce istek atmaya başlamış. Rate limiting olmasaydı tüm backend çökerdi. İşte bu yüzden rate limiting hem güvenlik hem de kararlılık için kritik.

# redis-rate-limiter.sh
# Redis tabanlı sliding window rate limiting test scripti

REDIS_HOST=${REDIS_HOST:-"localhost"}
REDIS_PORT=${REDIS_PORT:-6379}
SERVICE_NAME=$1
CLIENT_IP=$2
LIMIT=${3:-100}     # Dakikada maksimum istek
WINDOW=${4:-60}     # Saniye cinsinden pencere

if [ -z "$SERVICE_NAME" ] || [ -z "$CLIENT_IP" ]; then
    echo "Kullanım: $0 <servis-adi> <client-ip> [limit] [window]"
    exit 1
fi

CURRENT_TIME=$(date +%s%3N)  # Millisaniye
WINDOW_START=$((CURRENT_TIME - (WINDOW * 1000)))
KEY="rate_limit:${SERVICE_NAME}:${CLIENT_IP}"

# Atomic sliding window check
RESULT=$(redis-cli -h $REDIS_HOST -p $REDIS_PORT EVAL "
    local key = KEYS[1]
    local now = tonumber(ARGV[1])
    local window_start = tonumber(ARGV[2])
    local limit = tonumber(ARGV[3])
    local window = tonumber(ARGV[4])

    -- Eski kayıtları temizle
    redis.call('ZREMRANGEBYSCORE', key, 0, window_start)

    -- Mevcut istek sayısını al
    local count = redis.call('ZCARD', key)

    if count >= limit then
        return {0, count}
    end

    -- Yeni isteği ekle
    redis.call('ZADD', key, now, now)
    redis.call('EXPIRE', key, window)

    return {1, count + 1}
" 1 $KEY $CURRENT_TIME $WINDOW_START $LIMIT $WINDOW)

ALLOWED=$(echo $RESULT | awk '{print $1}')
COUNT=$(echo $RESULT | awk '{print $2}')

if [ "$ALLOWED" -eq 1 ]; then
    echo "İzin verildi. Mevcut count: $COUNT/$LIMIT"
    exit 0
else
    echo "Rate limit aşıldı! Count: $COUNT/$LIMIT"
    exit 429
fi

OAuth 2.0 Scope Tabanlı Yetkilendirme

Kimlik doğrulama ile yetkilendirmeyi birbirinden ayırmak, microservice güvenliğinin en önemli prensiplerinden biri. “Kullanıcı kim?” sorusu kimlik doğrulamanın, “Bu kullanıcı ne yapabilir?” sorusu ise yetkilendirmenin konusu.

#!/bin/bash
# scope-checker.sh
# JWT içindeki scope'ları kontrol eden script

JWT_TOKEN=$1
REQUIRED_SCOPE=$2

if [ -z "$JWT_TOKEN" ] || [ -z "$REQUIRED_SCOPE" ]; then
    echo "Kullanım: $0 <jwt_token> <gerekli_scope>"
    exit 1
fi

# Payload'ı decode et
PAYLOAD=$(echo $JWT_TOKEN | cut -d'.' -f2)
MOD4=$((${#PAYLOAD} % 4))
[ $MOD4 -eq 2 ] && PAYLOAD="${PAYLOAD}=="
[ $MOD4 -eq 3 ] && PAYLOAD="${PAYLOAD}="

DECODED=$(echo "$PAYLOAD" | tr '_-' '/+' | base64 -d 2>/dev/null)

# Scope'ları çıkar ve kontrol et
SCOPES=$(echo $DECODED | python3 -c "
import sys, json
data = json.load(sys.stdin)
scopes = data.get('scope', data.get('scopes', ''))
if isinstance(scopes, list):
    print(' '.join(scopes))
else:
    print(scopes)
")

echo "Mevcut scope'lar: $SCOPES"
echo "Gerekli scope: $REQUIRED_SCOPE"

if echo "$SCOPES" | grep -qw "$REQUIRED_SCOPE"; then
    echo "SONUÇ: Erişim izni VAR"
    exit 0
else
    echo "SONUÇ: Erişim izni YOK - 403 Forbidden"
    exit 403
fi

Scope hiyerarşisi tasarlarken dikkat ettiğim noktalar:

  • En az yetki prensibi: Her servis sadece ihtiyacı olan scope’ları talep etmeli
  • Kaynak bazlı scope: orders:read, orders:write gibi granüler yapı
  • Servis hesapları: İnsan kullanıcılardan ayrı, servisler için özel client credentials
  • Scope devralma: Admin scope’u alt scope’ları otomatik kapsamalı mı? Açıkça tanımla

Secret Yönetimi: HashiCorp Vault Entegrasyonu

Microservice’lerin en büyük güvenlik sorunlarından biri database şifreleri, API anahtarları gibi sırların yönetimi. .env dosyalarına veya environment variable’lara şifre koymak, Docker image’lara secret gömmek gerçek hayatta sıkça yapılan hatalar. Production’da bunun için Vault kullanmak adeta zorunlu hale geldi.

#!/bin/bash
# vault-secret-fetch.sh
# Servis başlangıcında Vault'tan secret çeken script

VAULT_ADDR=${VAULT_ADDR:-"https://vault.internal:8200"}
SERVICE_NAME=$1
SECRET_PATH="secret/microservices/${SERVICE_NAME}"

# Kubernetes Service Account ile Vault kimlik doğrulama
vault_login_k8s() {
    JWT=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)

    VAULT_TOKEN=$(curl -s 
        --request POST 
        --data "{"jwt": "${JWT}", "role": "${SERVICE_NAME}"}" 
        "${VAULT_ADDR}/v1/auth/kubernetes/login" | 
        python3 -c "import sys,json; print(json.load(sys.stdin)['auth']['client_token'])")

    export VAULT_TOKEN=$VAULT_TOKEN
    echo "Vault'a başarıyla giriş yapıldı"
}

# Secret'ları çek ve environment variable olarak export et
fetch_secrets() {
    echo "Secret'lar çekiliyor: $SECRET_PATH"

    SECRETS=$(curl -s 
        --header "X-Vault-Token: $VAULT_TOKEN" 
        "${VAULT_ADDR}/v1/${SECRET_PATH}" | 
        python3 -c "
import sys, json
data = json.load(sys.stdin)
secrets = data.get('data', {}).get('data', data.get('data', {}))
for key, value in secrets.items():
    print(f'export {key}="{value}"')
")

    if [ -z "$SECRETS" ]; then
        echo "HATA: Secret'lar çekilemedi!"
        exit 1
    fi

    # Güvenli temp dosyaya yaz
    SECRET_FILE=$(mktemp)
    echo "$SECRETS" > $SECRET_FILE
    chmod 600 $SECRET_FILE

    echo "Secret'lar başarıyla alındı: $(echo "$SECRETS" | wc -l) adet"
    echo "Yüklemek için: source $SECRET_FILE"
}

vault_login_k8s
fetch_secrets

Güvenlik Açıklarını Test Etme

Güvenliği sadece kurmak yetmez, düzenli olarak test etmek gerekiyor. Ben her sprint sonunda otomatik güvenlik testleri çalıştırıyorum.

#!/bin/bash
# api-security-test.sh
# Temel API güvenlik kontrolleri

API_URL=$1
TOKEN=$2

if [ -z "$API_URL" ]; then
    echo "Kullanım: $0 <api_url> [token]"
    exit 1
fi

PASS=0
FAIL=0

check() {
    local test_name=$1
    local result=$2
    local expected=$3

    if [ "$result" -eq "$expected" ]; then
        echo "PASS: $test_name (HTTP $result)"
        PASS=$((PASS + 1))
    else
        echo "FAIL: $test_name (Beklenen: $expected, Alınan: $result)"
        FAIL=$((FAIL + 1))
    fi
}

echo "=== API Güvenlik Testi: $API_URL ==="
echo ""

# 1. Token olmadan erişim engellenmeli
STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$API_URL/api/v1/orders")
check "Token olmadan 401 dönmeli" $STATUS 401

# 2. Geçersiz token reddedilmeli
STATUS=$(curl -s -o /dev/null -w "%{http_code}" 
    -H "Authorization: Bearer invalidtoken123" 
    "$API_URL/api/v1/orders")
check "Geçersiz token 401 dönmeli" $STATUS 401

# 3. HTTP redirect kontrolü (HTTPS zorunlu olmalı)
HTTP_URL=$(echo $API_URL | sed 's/https/http/')
STATUS=$(curl -s -o /dev/null -w "%{http_code}" --max-redirs 0 "$HTTP_URL/api/v1/orders")
check "HTTP'den HTTPS'e redirect (301)" $STATUS 301

# 4. SQL injection denemesi
if [ ! -z "$TOKEN" ]; then
    STATUS=$(curl -s -o /dev/null -w "%{http_code}" 
        -H "Authorization: Bearer $TOKEN" 
        "$API_URL/api/v1/orders?id=1' OR '1'='1")
    check "SQL injection koruması (400)" $STATUS 400
fi

# 5. Büyük payload kontrolü
BIG_PAYLOAD=$(python3 -c "print('A' * 1048577)")  # 1MB + 1 byte
STATUS=$(curl -s -o /dev/null -w "%{http_code}" 
    -X POST 
    -H "Content-Type: application/json" 
    -H "Authorization: Bearer ${TOKEN:-invalid}" 
    -d "{"data": "$BIG_PAYLOAD"}" 
    "$API_URL/api/v1/orders")
check "Büyük payload reddi (413)" $STATUS 413

# 6. Security header kontrolü
HEADERS=$(curl -s -I "$API_URL" 2>/dev/null)
if echo "$HEADERS" | grep -qi "Strict-Transport-Security"; then
    echo "PASS: HSTS header mevcut"
    PASS=$((PASS + 1))
else
    echo "FAIL: HSTS header eksik!"
    FAIL=$((FAIL + 1))
fi

echo ""
echo "=== Test Sonuçları ==="
echo "Geçti: $PASS"
echo "Kaldı: $FAIL"
[ $FAIL -eq 0 ] && echo "Durum: TAMAM" || echo "Durum: DİKKAT GEREKİYOR"

Gözlemlenebilirlik ve Güvenlik Logları

Güvenlik olaylarını tespit edebilmek için doğru loglama kritik. Her API isteğini yapılandırılmış şekilde loglamak, anomali tespitini kolaylaştırır.

Bir log pipeline kurarken dikkat ettiğim noktalar:

  • Correlation ID: Her istek için benzersiz ID üret ve tüm servislerde bu ID’yi taşı. Bir saldırı zincirini takip etmek çok kolaylaşıyor.
  • Hassas veri maskeleme: Authorization header’ını, şifre alanlarını loglarına asla tam olarak yazma. Bearer eyJ... yerine Bearer [MASKED] yaz.
  • Başarısız kimlik doğrulama alarmı: Aynı IP’den 5 dakika içinde 10’dan fazla 401 geliyorsa alarm üret.
  • Log tampering koruması: Logları doğrudan saldırganın erişebileceği sunucuda tutma. Merkezi log sunucusuna gönder.
  • Retention politikası: Güvenlik loglarını en az 90 gün sakla, mümkünse 1 yıl. Yasal zorunluluklar da var bu konuda.

Pratik Güvenlik Kontrol Listesi

Microservice API güvenliğini production’a almadan önce kontrol ettiğim maddeler:

  • TLS 1.2+ zorunlu, TLS 1.0 ve 1.1 devre dışı, HTTP tamamen kapalı
  • JWT imzalama algoritması RS256 veya ES256, HS256 sadece zorunlu hallerde
  • Token ömrü: Access token max 15 dakika, refresh token max 24 saat
  • Rate limiting hem IP bazlı hem de kullanıcı bazlı uygulanmış olmalı
  • mTLS veya en azından servis-to-servis iletişimde API key doğrulama
  • Secret’lar environment variable yerine Vault veya benzeri araçta
  • Input validation her servis kendi başına yapmalı, gateway’e güvenmemeli
  • CORS konfigürasyonu production’da * asla kullanılmamalı
  • Dependency scanning kullanılan kütüphanelerde CVE kontrolü otomatize edilmeli
  • Penetration test yılda en az bir kez, büyük değişiklik sonrası tekrar

Sonuç

Microservice API güvenliği bir kez yapıp geçilen bir iş değil. Sürekli izlenmesi, test edilmesi ve güncellenmesi gereken canlı bir sistem. En büyük hata, “bu iç ağda zaten” veya “gateway halleder” diyerek katmanlı güvenliği es geçmek.

Benim deneyimimde en kritik üç alan şunlar: servisler arası kimlik doğrulama için mTLS, secret yönetimi için Vault, ve düzenli otomatik güvenlik testleri. Bu üçünü doğru kurduğunda geri kalan her şey bunların üzerine inşa edilebilir.

Başlangıç için hepsini birden kurmaya çalışma. Önce API Gateway’ini sağlam kur ve JWT standardını oturut. Sonra rate limiting ekle. Ardından servisler arası mTLS’e geç. Vault entegrasyonunu da bu sırada yapabilirsin. Güvenlik katmanları peyderpey eklendikçe hem sistemin kararlılığı artar hem de ekibin bu araçlara adaptasyonu sağlanmış olur.

Sorularınız veya farklı yaklaşımlarınız varsa yorumlarda buluşalım.

Bir yanıt yazın

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