JWT ile Çoklu Servis Kimlik Doğrulama: Mikroservis Mimarisi

Mikroservis mimarisine geçtiğinizde ilk kafanızı ağrıtan şeylerden biri kimlik doğrulamadır. Monolitik uygulamada session tabanlı auth yeterliydi, bir veritabanına bakıyordunuz, kullanıcı giriş yapmış mı değil mi anlıyordunuz. Ama artık elimizde onlarca servis var: kullanıcı servisi, sipariş servisi, ödeme servisi, bildirim servisi… Her biri ayrı bir container’da çalışıyor, bazıları farklı dillerde yazılmış. Bu servislerin birbiriyle ve dış dünyayla güvenli iletişim kurması için JWT tam anlamıyla kurtarıcı oluyor.

Bu yazıda JWT’yi sadece teorik olarak değil, gerçek dünya mikroservis senaryolarında nasıl kullanacağınızı, servisler arası iletişimi nasıl güvenli hale getireceğinizi ve production’da başınıza gelebilecek sorunları nasıl önleyeceğinizi anlatacağım.

JWT Nedir, Neden Mikroservisler İçin İdeal?

JWT (JSON Web Token), üç parçadan oluşan, imzalanmış bir token formatıdır: header, payload ve signature. Base64URL ile encode edilmiş bu üç parça nokta ile ayrılır. Önemli olan nokta şu: JWT stateless’tır. Yani token’ı doğrulamak için merkezi bir veritabanına sorgu atmak zorunda değilsiniz.

Mikroservis mimarisinde bu inanılmaz değerlidir. Sipariş servisi, kullanıcının kimliğini doğrulamak için kullanıcı servisine HTTP isteği atmak yerine elindeki public key ile token’ı kendi başına doğrulayabilir. Bu hem gecikmeyi azaltır hem de servisler arası bağımlılığı minimuma indirir.

Payload içinde istediğiniz claim’leri taşıyabilirsiniz:

# JWT'yi decode etmek için basit bir one-liner
echo "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyXzEyMyIsInJvbGUiOiJhZG1pbiIsInNlcnZpY2VzIjpbIm9yZGVyIiwicGF5bWVudCJdLCJpYXQiOjE3MDAwMDAwMDAsImV4cCI6MTcwMDAwMzYwMH0.signature" | 
cut -d'.' -f2 | 
base64 -d 2>/dev/null | 
python3 -m json.tool

Temel Mimari: Auth Servisi Merkezi, Doğrulama Dağıtık

Klasik yaklaşım şöyle çalışır: Merkezi bir Auth servisi token üretir, diğer tüm servisler bu token’ları bağımsız olarak doğrular.

[Client] --> [API Gateway] --> [Auth Service] (token üretir)
                |
                v
    [Order Service] (token doğrular)
    [Payment Service] (token doğrular)  
    [Notification Service] (token doğrular)

Bu yaklaşım için asimetrik şifreleme (RS256 veya ES256) kullanmak zorundasınız. Simetrik HMAC256 kullanırsanız tüm servislerinizin secret key’i bilmesi gerekir, bu hem güvenlik açığı hem de key rotation kabusudur.

Auth Servisini Ayağa Kaldırmak

Auth servisinizi Node.js ile yazıyorsanız basit bir örnek:

# Gerekli paketleri yükle
npm install jsonwebtoken express bcrypt dotenv

# RSA key pair oluştur
mkdir -p /opt/auth-service/keys
cd /opt/auth-service/keys

# Private key (sadece auth serviste olacak)
openssl genrsa -out private.pem 2048

# Public key (tüm servislere dağıtılacak)
openssl rsa -in private.pem -pubout -out public.pem

# İzinleri sıkılaştır
chmod 600 private.pem
chmod 644 public.pem
# auth-service/src/token.js için temel token üretme mantığı
cat > /opt/auth-service/src/token.js << 'EOF'
const jwt = require('jsonwebtoken');
const fs = require('fs');

const privateKey = fs.readFileSync('/opt/auth-service/keys/private.pem');
const publicKey = fs.readFileSync('/opt/auth-service/keys/public.pem');

function generateToken(userId, roles, allowedServices) {
  const payload = {
    sub: userId,
    roles: roles,
    services: allowedServices,  // Hangi servislere erişebileceği
    iss: 'auth.mycompany.internal',
    aud: allowedServices,
    iat: Math.floor(Date.now() / 1000),
    jti: require('crypto').randomUUID()  // Unique token ID
  };

  return jwt.sign(payload, privateKey, {
    algorithm: 'RS256',
    expiresIn: '1h'
  });
}

function verifyToken(token, audience) {
  return jwt.verify(token, publicKey, {
    algorithms: ['RS256'],
    issuer: 'auth.mycompany.internal',
    audience: audience
  });
}

module.exports = { generateToken, verifyToken };
EOF

Servisler Arası İletişimde JWT

Burada iki farklı senaryo var ve ikisini karıştırmamak lazım:

User-to-Service: Kullanıcı bir istek atıyor, bu istek birden fazla servise ulaşıyor.

Service-to-Service: Bir servis başka bir servisi tetikliyor, ortada kullanıcı yok.

User-to-Service Senaryosu

API Gateway’iniz token’ı alır, doğrular ve downstream servislere forward eder. Servislerin de kendi içlerinde doğrulama yapması gerekir, çünkü API Gateway’e güvenmek yetmez. Defense in depth prensibi.

# Python'da servis tarafı middleware örneği (FastAPI)
cat > /opt/order-service/middleware/auth.py << 'EOF'
import jwt
from fastapi import HTTPException, Security
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials

PUBLIC_KEY_PATH = "/opt/shared-keys/public.pem"
with open(PUBLIC_KEY_PATH, 'r') as f:
    PUBLIC_KEY = f.read()

security = HTTPBearer()

def verify_jwt(credentials: HTTPAuthorizationCredentials = Security(security)):
    token = credentials.credentials
    try:
        payload = jwt.decode(
            token,
            PUBLIC_KEY,
            algorithms=["RS256"],
            issuer="auth.mycompany.internal",
            audience="order"  # Bu servisin audience değeri
        )
        
        # Role kontrolü
        if "order:read" not in payload.get("roles", []):
            raise HTTPException(status_code=403, detail="Yetersiz yetki")
            
        return payload
    except jwt.ExpiredSignatureError:
        raise HTTPException(status_code=401, detail="Token süresi dolmuş")
    except jwt.InvalidAudienceError:
        raise HTTPException(status_code=403, detail="Bu servise erişim yetkisi yok")
    except jwt.JWTError as e:
        raise HTTPException(status_code=401, detail=f"Geçersiz token: {str(e)}")
EOF

Service-to-Service Senaryosu

Sipariş servisi ödeme servisini çağırdığında, kullanıcının token’ını forward etmek kötü bir pratiktir. Bunun yerine her servis kendi “service account” token’ını kullanmalı.

# Servis hesapları için özel token üretme scripti
cat > /opt/scripts/generate-service-token.sh << 'EOF'
#!/bin/bash

SERVICE_NAME=$1
ALLOWED_SERVICES=$2

if [ -z "$SERVICE_NAME" ]; then
    echo "Kullanim: $0 <service-name> <allowed-services-csv>"
    exit 1
fi

# Python ile service token üret
python3 << PYEOF
import jwt
import time
import json
import uuid

with open('/opt/auth-service/keys/private.pem', 'r') as f:
    private_key = f.read()

payload = {
    "sub": f"service:${SERVICE_NAME}",
    "type": "service_account",
    "roles": ["internal-service"],
    "services": "${ALLOWED_SERVICES}".split(",") if "${ALLOWED_SERVICES}" else [],
    "iss": "auth.mycompany.internal",
    "aud": "${ALLOWED_SERVICES}".split(",") if "${ALLOWED_SERVICES}" else [],
    "iat": int(time.time()),
    "exp": int(time.time()) + 3600,
    "jti": str(uuid.uuid4())
}

token = jwt.encode(payload, private_key, algorithm='RS256')
print(token)
PYEOF
EOF

chmod +x /opt/scripts/generate-service-token.sh

# Örnek kullanım:
# ./generate-service-token.sh order-service payment,notification

Token Dağıtımı: Public Key’leri Servislere Nasıl Ulaştırırsınız?

Bu kısmı es geçen çok mühendis gördüm, sonra production’da başları yanıyor. Public key’leri servislere dağıtmanın birkaç yolu var:

Yöntem 1: JWKS Endpoint

En temiz yöntem bu. Auth servisiniz bir JWKS (JSON Web Key Set) endpoint’i sunar, diğer servisler public key’i bu endpoint’ten çeker.

# Nginx ile basit JWKS endpoint
cat > /etc/nginx/conf.d/jwks.conf << 'EOF'
server {
    listen 8080;
    server_name auth.mycompany.internal;
    
    location /.well-known/jwks.json {
        alias /opt/auth-service/keys/jwks.json;
        add_header Content-Type application/json;
        add_header Cache-Control "public, max-age=3600";
    }
}
EOF

# JWKS dosyasını oluştur
cat > /opt/scripts/generate-jwks.py << 'EOF'
#!/usr/bin/env python3
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.backends import default_backend
import base64
import json
import struct

with open('/opt/auth-service/keys/public.pem', 'rb') as f:
    public_key = serialization.load_pem_public_key(f.read(), backend=default_backend())

public_numbers = public_key.public_key().public_numbers() if hasattr(public_key, 'public_key') else public_key.public_numbers()

def int_to_base64url(n):
    byte_length = (n.bit_length() + 7) // 8
    n_bytes = n.to_bytes(byte_length, byteorder='big')
    return base64.urlsafe_b64encode(n_bytes).rstrip(b'=').decode('ascii')

jwks = {
    "keys": [{
        "kty": "RSA",
        "use": "sig",
        "kid": "key-v1-2024",
        "alg": "RS256",
        "n": int_to_base64url(public_numbers.n),
        "e": int_to_base64url(public_numbers.e)
    }]
}

with open('/opt/auth-service/keys/jwks.json', 'w') as f:
    json.dump(jwks, f, indent=2)
    
print("JWKS dosyasi olusturuldu")
EOF

python3 /opt/scripts/generate-jwks.py

Yöntem 2: Kubernetes Secret ile Dağıtım

K8s ortamındaysanız public key’i bir Secret olarak saklayıp her servise mount edebilirsiniz:

# Public key'i K8s secret olarak kaydet
kubectl create secret generic jwt-public-key 
  --from-file=public.pem=/opt/auth-service/keys/public.pem 
  -n production

# Deployment'a mount et
cat > /tmp/order-service-patch.yaml << 'EOF'
spec:
  template:
    spec:
      containers:
      - name: order-service
        volumeMounts:
        - name: jwt-public-key
          mountPath: /opt/keys
          readOnly: true
      volumes:
      - name: jwt-public-key
        secret:
          secretName: jwt-public-key
EOF

kubectl patch deployment order-service 
  -n production 
  --patch-file /tmp/order-service-patch.yaml

Token Yenileme ve Revoke Stratejisi

JWT’nin stateless doğası bir paradoks yaratır: token’ı veritabanına sormadan doğrulayabilirsiniz ama token’ı expire etmeden önce geçersiz de kılamazsınız. Kullanıcı şifresini değiştirdi, hesabı askıya alındı, ama elindeki token hala bir saat geçerli.

Bunu çözmenin birkaç yolu var:

Access Token + Refresh Token kombinasyonu: Access token’ı kısa tutun (5-15 dakika), refresh token’ı uzun tutun ve bunu veritabanında saklayın.

Token blacklist: Redis’te geçersiz token ID’lerini saklayın.

# Redis tabanlı token blacklist
cat > /opt/auth-service/src/blacklist.sh << 'EOF'
#!/bin/bash
# Token revoke scripti - manuel müdahale için

REDIS_HOST=${REDIS_HOST:-"redis.mycompany.internal"}
TOKEN_JTI=$1
EXPIRE_SECONDS=${2:-3600}

if [ -z "$TOKEN_JTI" ]; then
    echo "Kullanim: $0 <token-jti> [expire-seconds]"
    exit 1
fi

redis-cli -h $REDIS_HOST SET "blacklist:$TOKEN_JTI" "revoked" EX $EXPIRE_SECONDS

if [ $? -eq 0 ]; then
    echo "Token $TOKEN_JTI basariyla blacklist'e alindi (TTL: ${EXPIRE_SECONDS}s)"
else
    echo "HATA: Token blacklist'e alinamadi!"
    exit 1
fi
EOF

chmod +x /opt/auth-service/src/blacklist.sh

# Redis'te blacklist kontrolü için Python middleware eki
cat >> /opt/order-service/middleware/auth.py << 'EOF'

import redis

redis_client = redis.Redis(host='redis.mycompany.internal', port=6379, decode_responses=True)

def check_blacklist(jti: str) -> bool:
    """True dönerse token blacklist'te, erişimi reddet"""
    return redis_client.exists(f"blacklist:{jti}") > 0
EOF

Production’da Başınıza Gelebilecek Sorunlar

Teorik bilgi yeterli değil, production’da gördüğüm gerçek problemleri paylaşayım.

Saat Senkronizasyonu

JWT doğrulaması zaman damgasına dayanır. Servislerinizin saatleri birbirinden farklıysa geçerli token’lar “süresi dolmuş” görünebilir.

# Tüm node'larda NTP durumunu kontrol et
for host in auth-service order-service payment-service; do
    echo "=== $host ==="
    ssh $host "chronyc tracking | grep -E 'System time|RMS offset'"
done

# Maksimum kabul edilebilir kayma 500ms, fazlası alarm vermeli
# Monitoring için Prometheus alert örneği:
cat > /opt/monitoring/alerts/ntp-drift.yaml << 'EOF'
groups:
- name: jwt-prerequisites
  rules:
  - alert: NTPDriftHigh
    expr: abs(node_timex_offset_seconds) > 0.5
    for: 2m
    labels:
      severity: warning
    annotations:
      summary: "NTP kayması yüksek - JWT doğrulaması etkilenebilir"
      description: "{{ $labels.instance }} üzerinde NTP kayması {{ $value }}s"
EOF

Key Rotation

6 ayda bir veya güvenlik ihlali durumunda private key’inizi değiştirmeniz gerekir. Bunu yaparken downtime yaşamamalısınız.

#!/bin/bash
# /opt/scripts/rotate-jwt-keys.sh
# Graceful key rotation scripti

KEY_DIR="/opt/auth-service/keys"
DATE=$(date +%Y%m%d)

echo "JWT Key Rotation Basliyor - $DATE"

# 1. Yeni key pair olustur
openssl genrsa -out $KEY_DIR/private-${DATE}.pem 2048
openssl rsa -in $KEY_DIR/private-${DATE}.pem -pubout -out $KEY_DIR/public-${DATE}.pem

# 2. JWKS dosyasini IKISI de icine alacak sekilde guncelle
# (Gecis suresi boyunca eski token'lar da gecerli olmali)
python3 /opt/scripts/generate-jwks.py --include-old-key

# 3. Auth servisi yeni key'i kullanmaya baslasın
ln -sf $KEY_DIR/private-${DATE}.pem $KEY_DIR/private.pem
ln -sf $KEY_DIR/public-${DATE}.pem $KEY_DIR/public.pem

# 4. Auth servisi restart (graceful)
systemctl reload auth-service || kubectl rollout restart deployment/auth-service -n production

# 5. 2 saat bekle (eski token'larin expire olmasi icin)
echo "Eski token'larin expire olmasi bekleniyor (2 saat)..."
sleep 7200

# 6. JWKS'ten eski key'i kaldir
python3 /opt/scripts/generate-jwks.py --remove-old-key

echo "Key rotation tamamlandi"

Token Boyutu

Payload’a aşırı veri doldurmak her HTTP isteğini şişirir. Her request header’ında bu token taşınıyor, CDN ve proxy loglarına düşüyor.

# Token boyutunu kontrol et
TOKEN=$(cat /tmp/test-token.txt)
TOKEN_SIZE=$(echo -n $TOKEN | wc -c)
echo "Token boyutu: ${TOKEN_SIZE} bytes"

# 1KB'ı geçen token'lar için uyarı
if [ $TOKEN_SIZE -gt 1024 ]; then
    echo "UYARI: Token boyutu ${TOKEN_SIZE} bytes, optimize edilmeli!"
    echo "Payload icerigi:"
    echo $TOKEN | cut -d'.' -f2 | base64 -d 2>/dev/null | python3 -m json.tool
fi

Monitoring ve Debugging

# JWT ile ilgili hataları merkezi olarak toplamak için
# Fluentd/Filebeat filter örneği - önemli alanları çıkar
cat > /opt/logging/jwt-filter.conf << 'EOF'
<filter microservices.**>
  @type record_transformer
  enable_ruby true
  <record>
    jwt_error ${record.dig("error", "type")}
    jwt_subject ${
      begin
        require 'base64'
        require 'json'
        token = record["headers"]["authorization"].to_s.split(" ").last
        payload = JSON.parse(Base64.decode64(token.split(".")[1] + "==")) rescue {}
        payload["sub"]
      rescue
        "unknown"
      end
    }
    service_name ${tag_parts[1]}
  </record>
</filter>
EOF

# Auth hata metriklerini Grafana'da izlemek için query örneği:
# sum by (service_name, jwt_error) (
#   rate(http_requests_total{status=~"401|403"}[5m])
# )

Sıkça Yapılan Hatalar

Pratikte gördüğüm ve sizi production’da uğraştıracak hatalar:

  • Algorithm confusion saldırısı: Doğrulama tarafında algorithms=["RS256"] olarak sabitleyin, asla algorithms=["*"] veya library’nin default değerine güvenmeyin. Bazı eski kütüphaneler “none” algoritmasını kabul ediyor.
  • Token’ı cookie’de saklamak vs Authorization header: Mobile client’lar için header daha uygun, web için HttpOnly secure cookie daha güvenli. İkisini karıştırmayın.
  • Tüm servislere aynı audience izni vermek: Her serviste audience kontrolü yapın. Ödeme servisine gidecek token farklı, sipariş servisine gidecek token farklı olmalı.
  • Secret key’i environment variable olarak koyup loglamak: Private key’i asla process argümanı olarak geçirmeyin, dosyadan okuyun.
  • Refresh token’ı JWT yapmak: Refresh token’ı opaque (rastgele string) yapın ve veritabanında saklayın. JWT olursa revoke edemezsiniz.

Sonuç

JWT tabanlı mikroservis kimlik doğrulaması doğru uygulandığında hem güvenli hem de performanslıdır. Asimetrik şifreleme kullanarak auth servisinizi doğrulama sürecinden izole edin, her servise sadece ihtiyacı olan audience iznini verin ve key rotation stratejinizi deployment’tan önce test edin.

En önemli pratik tavsiyem: token boyutunu küçük tutun, saat senkronizasyonunu ihmal etmeyin ve her servisin kendi doğrulamasını yapmasını sağlayın. API Gateway’e kör güven, savunma mimarisinin en büyük düşmanıdır.

Production’a çıkmadan önce şunları mutlaka test edin: süresi dolmuş token davranışı, yanlış audience ile istek, revoke edilmiş token, key rotation sırasında geçiş dönemi. Bu senaryolar test ortamında can sıkıcı, production’da ise çok daha pahalıdır.

Bir yanıt yazın

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