JWT Token Güvenliği: İmzalama ve Şifreleme Yöntemleri
API güvenliği konusunda en çok yanlış anlaşılan konulardan biri JWT’nin nasıl çalıştığı ve “imzalanmış” ile “şifrelenmiş” token’ların arasındaki fark. Bir çok geliştirici ve sysadmin, JWT imzalandığında içeriğin de gizlendiğini sanıyor. Bu yanılgı production sistemlerde ciddi güvenlik açıklarına yol açabiliyor. Gelin bu konuyu baştan sona, gerçek dünya senaryolarıyla birlikte ele alalım.
JWT Nedir ve Nasıl Çalışır
JWT (JSON Web Token), üç bölümden oluşan ve nokta ile ayrılan bir token formatıdır: Header, Payload ve Signature. Yapı şöyle görünür:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkFobWV0IFlpbG1heiIsInJvbGUiOiJhZG1pbiJ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Her bölüm Base64URL ile encode edilmiştir. Base64 şifreleme değildir, sadece binary veriyi text formatına çevirme yöntemidir. Herhangi biri bu token’ı alıp base64 -d komutuyla veya jwt.io sitesine yapıştırarak içeriği okuyabilir.
# Token'ın payload kısmını decode etmek (ikinci bölüm)
echo "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkFobWV0IFlpbG1heiIsInJvbGUiOiJhZG1pbiJ9" | base64 -d
# Çıktı: {"sub":"1234567890","name":"Ahmet Yilmaz","role":"admin"}
Bunu görünce şunu anlamalısınız: Token’ınızın payload kısmına koyduğunuz her şey açık metin olarak okunabilir. Şifre, kredi kartı numarası, hassas kişisel veri koymak ciddi bir güvenlik açığıdır.
İmzalama (JWS) vs Şifreleme (JWE)
JWT dünyasında iki farklı konsept var ve bunları birbirine karıştırmak çok yaygın.
JWS (JSON Web Signature) ile imzalanmış token’lar içeriği gizlemez, sadece bütünlüğü sağlar. Yani token’ın değiştirilip değiştirilmediğini anlarsınız ama içeriği herkes okuyabilir.
JWE (JSON Web Encryption) ile şifrelenmiş token’lar ise içeriği gerçekten gizler. Sadece doğru anahtara sahip olan taraf payload’ı okuyabilir.
Büyük çoğunlukta kullanılan HS256, RS256 gibi algoritmalar JWS kategorisindedir. JWE çok daha az kullanılır ama hassas veri taşımanız gerekiyorsa tercih edilmesi gerekir.
Simetrik İmzalama: HMAC Algoritmaları
HS256, HS384 ve HS512 simetrik algoritmalardır. Aynı secret key hem imzalamak hem de doğrulamak için kullanılır.
# OpenSSL ile güçlü bir HMAC secret üretmek
openssl rand -base64 64
# Veya /dev/urandom kullanarak
cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 64 | head -n 1
Simetrik imzalamanın en büyük sorunu şu: Hem token üreten hem de doğrulayan taraf aynı secret’ı bilmek zorunda. Microservice mimarisinde 10 farklı servis JWT doğruluyorsa, 10 servis de bu secret’ı biliyor demektir. Bir servis compromise olduğunda tüm sistemi tehdit eder.
# Node.js ile basit JWT üretimi (test amaçlı script)
node -e "
const crypto = require('crypto');
const header = Buffer.from(JSON.stringify({alg:'HS256',typ:'JWT'})).toString('base64url');
const payload = Buffer.from(JSON.stringify({sub:'user123',role:'admin',exp:Math.floor(Date.now()/1000)+3600})).toString('base64url');
const signature = crypto.createHmac('sha256','super-secret-key').update(header+'.'+payload).digest('base64url');
console.log(header+'.'+payload+'.'+signature);
"
Asimetrik İmzalama: RSA ve ECDSA
RS256, RS384, RS512 (RSA) ve ES256, ES384, ES512 (ECDSA) asimetrik algoritmalardır. Private key ile imzalar, public key ile doğrularsınız.
Bu yaklaşımın avantajı şu: Token üretme yetkisi sadece private key’e sahip olan auth servisindedir. Diğer tüm servisler sadece public key ile doğrulama yapar. Public key’i bilmek token üretmeye yetmez.
# RSA key pair üretimi
openssl genrsa -out private_key.pem 2048
openssl rsa -in private_key.pem -pubout -out public_key.pem
# ECDSA key pair üretimi (daha küçük key, aynı güvenlik seviyesi)
openssl ecparam -name prime256v1 -genkey -noout -out ec_private.pem
openssl ec -in ec_private.pem -pubout -out ec_public.pem
# Key içeriğini kontrol etme
openssl rsa -in private_key.pem -text -noout | head -20
Üretim ortamında RSA yerine ECDSA tercih etmenizi öneririm. Daha küçük key boyutu, daha hızlı işlem ve aynı güvenlik seviyesi sunuyor. 256-bit ECDSA, 3072-bit RSA ile karşılaştırılabilir güvenlik sağlar.
JWE ile Gerçek Şifreleme
Hassas veri taşımanız gerekiyorsa JWE kullanmalısınız. JWE beş bölümden oluşur: Header, Encrypted Key, Initialization Vector, Ciphertext ve Authentication Tag.
# Python ile JWE örneği (python-jose kütüphanesi)
python3 << 'EOF'
from jose import jwe
import json
# Şifrelenecek payload
payload = json.dumps({
"sub": "user123",
"email": "[email protected]",
"tc_no": "12345678901", # Hassas veri - normalde JWE olmadan koymayin!
"exp": 1700000000
})
# AES-256-GCM ile şifreleme
key = b"super-secret-32-byte-key-here!!!"
token = jwe.encrypt(payload, key, algorithm='dir', encryption='A256GCM')
print("JWE Token:", token.decode())
# Decrypt etme
decrypted = jwe.decrypt(token, key)
print("Decrypted:", decrypted)
EOF
Gerçek dünyada JWE kullanım senaryosu şöyle olabilir: Healthcare uygulamasında hasta ID’si, sigorta numarası gibi bilgileri microservice’ler arası iletişimde taşımak zorundaysanız JWE zorunlu hale gelir. GDPR ve KVKK kapsamında da bu tür verilerin açık taşınması uyumluluk ihlali sayılabilir.
Algorithm Confusion Saldırısı
Bu saldırı türü JWT güvenliğinde en kritik konulardan biridir. Saldırgan, RS256 ile imzalanmış bir token’ın header’ındaki algoritma değerini HS256 olarak değiştirir ve public key’i HMAC secret olarak kullanarak token’ı yeniden imzalar.
Eğer sunucu tarafında algoritma doğrulaması yapılmıyorsa bu sahte token geçerli kabul edilir. Public key zaten public olduğu için saldırgan bunu kolayca elde edebilir.
# Savunmasız yapılandırma örneği (YAPMAYIN)
# verify(token, public_key) # Algoritma kontrolü yok!
# Güvenli yapılandırma
# verify(token, public_key, algorithms=['RS256']) # Sadece RS256 kabul et
# nginx üzerinde JWT doğrulama için algoritma kısıtlaması
# nginx.conf içinde jwt_alg direktifi ile:
cat << 'EOF' >> /etc/nginx/conf.d/api.conf
location /api/ {
auth_jwt "API Zone";
auth_jwt_key_file /etc/nginx/jwt_public.pem;
# Sadece RS256 kabul et
auth_jwt_alg RS256;
}
EOF
Kütüphane seçiminde bu konuya dikkat edin. Bazı eski kütüphaneler alg: none değerini kabul eder, yani imzasız token’ları geçerli sayar. Bu durumda herhangi biri istediği payload ile geçerli token üretebilir.
Token Süresi ve Revocation Stratejileri
JWT’nin stateless olması hem avantaj hem dezavantaj. Token bir kez verildi mi, süresi dolana kadar geçerlidir. Kullanıcıyı “logout” etseniz bile token kullanılabilir durumda kalır.
# Redis ile token blacklist implementasyonu
# Logout olunduğunda token'ı blacklist'e ekle
redis-cli << 'EOF'
# Token'ın JTI (JWT ID) claim'ini blacklist'e ekle, token süresine kadar tut
SET "blacklist:jti:abc123def456" "revoked" EX 3600
# Kontrol et
GET "blacklist:jti:abc123def456"
EOF
# Token blacklist kontrol scripti
cat << 'SCRIPT' > /usr/local/bin/check_token_blacklist.sh
#!/bin/bash
JTI=$1
RESULT=$(redis-cli GET "blacklist:jti:$JTI")
if [ "$RESULT" == "revoked" ]; then
echo "Token revoked"
exit 1
fi
echo "Token valid"
exit 0
SCRIPT
chmod +x /usr/local/bin/check_token_blacklist.sh
Token süre yönetimi için önerdiğim strateji: Access token’ı kısa tutun (15 dakika ile 1 saat arası), refresh token’ı uzun tutun (7-30 gün) ve refresh token’ı sadece HTTP-only cookie olarak saklayın. Bu yaklaşım hem kullanıcı deneyimini hem de güvenliği dengeler.
Production Ortamında Key Yönetimi
Secret ve private key’lerin nasıl saklandığı kritik önem taşır. Kod içine gömmek, environment variable olarak düz metin saklamak büyük hatalar.
# HashiCorp Vault ile JWT secret yönetimi
# Vault'a JWT secret kaydetme
vault kv put secret/jwt/signing
private_key=@/tmp/private_key.pem
algorithm="RS256"
key_id="2024-prod-01"
# Vault'tan secret okuma (uygulama başlangıcında)
vault kv get -field=private_key secret/jwt/signing > /run/secrets/jwt_private_key
chmod 400 /run/secrets/jwt_private_key
# Secret'ı memory'e yükleme ve dosyayı silme
PRIVATE_KEY=$(vault kv get -field=private_key secret/jwt/signing)
export JWT_PRIVATE_KEY="$PRIVATE_KEY"
Kubernetes ortamında ise Sealed Secrets veya External Secrets Operator kullanmanızı öneririm. Plain Kubernetes Secret’ları base64 encode edilmiştir, şifreli değildir.
# Kubernetes Secret olarak JWT key tanımlama
kubectl create secret generic jwt-signing-key
--from-file=private_key=./private_key.pem
--from-file=public_key=./public_key.pem
--namespace=auth-service
# Secret'ı readonly mount etme
cat << 'EOF' | kubectl apply -f -
apiVersion: v1
kind: Pod
spec:
containers:
- name: auth-service
volumeMounts:
- name: jwt-keys
mountPath: /run/secrets/jwt
readOnly: true
volumes:
- name: jwt-keys
secret:
secretName: jwt-signing-key
defaultMode: 0400
EOF
JWKS Endpoint ile Public Key Dağıtımı
Büyük sistemlerde public key’i her servise manuel dağıtmak yerine JWKS (JSON Web Key Set) endpoint’i kullanın. Auth servisiniz /.well-known/jwks.json adresinde public key’leri yayınlar, diğer servisler bu endpoint’ten otomatik olarak alır.
# OpenSSL ile public key'i JWK formatına çevirme
# python-jwcrypto kütüphanesi ile
python3 << 'EOF'
from jwcrypto import jwk
import json
# PEM formatındaki public key'i oku
with open('public_key.pem', 'rb') as f:
key = jwk.JWK.from_pem(f.read())
# JWKS formatında export et
jwks = {"keys": [json.loads(key.export_public())]}
print(json.dumps(jwks, indent=2))
# Dosyaya kaydet
with open('/var/www/html/.well-known/jwks.json', 'w') as f:
json.dump(jwks, f, indent=2)
EOF
# nginx ile JWKS endpoint'i serve etme
cat << 'EOF' >> /etc/nginx/conf.d/auth.conf
location /.well-known/jwks.json {
alias /var/www/html/.well-known/jwks.json;
default_type application/json;
add_header Cache-Control "public, max-age=3600";
add_header Access-Control-Allow-Origin "*";
}
EOF
Key rotation senaryosunda JWKS endpoint’i çok işe yarar. Eski key’i hemen kaldırmak yerine yeni key’i ekleyin, her iki key’i bir süre beraber tutun. Aktif token’lar kid (Key ID) claim’i ile hangi key ile imzalandığını söyler. Eski token’lar süresi dolana kadar eski key ile doğrulanmaya devam eder.
Yaygın Güvenlik Hataları ve Çözümleri
Üretim ortamında sıkça gördüğüm hatalar:
Zayıf secret kullanımı: HS256 için “secret”, “password123” gibi tahmin edilebilir değerler. Minimum 256-bit random değer kullanın.
exp claim koymamak: Süresi hiç dolmayan token üretmek ciddi risk. Her token için uygun bir expiry set edin.
Tüm hassas veriyi payload’a koymak: E-posta adresi bile GDPR kapsamında hassas sayılabilir. Payload’ı minimize edin, sadece gerekli claim’leri koyun.
HTTPS kullanmamak: JWT imzalı olsa da MITM saldırısında token çalınabilir ve süresi dolana kadar kullanılabilir.
Client-side storage güvenliği: localStorage’da saklanan token’lar XSS saldırısına açık. HTTP-only cookie daha güvenli.
# Token güvenlik denetimi için basit bir bash scripti
cat << 'SCRIPT' > /usr/local/bin/audit_jwt.sh
#!/bin/bash
TOKEN=$1
if [ -z "$TOKEN" ]; then
echo "Kullanim: $0 <jwt_token>"
exit 1
fi
# Header ve payload decode et
HEADER=$(echo $TOKEN | cut -d'.' -f1 | base64 -d 2>/dev/null)
PAYLOAD=$(echo $TOKEN | cut -d'.' -f2 | base64 -d 2>/dev/null)
echo "=== JWT Audit ==="
echo "Header: $HEADER"
echo "Payload: $PAYLOAD"
# Algoritma kontrolu
ALG=$(echo $HEADER | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('alg','MISSING'))")
echo ""
echo "Algoritma: $ALG"
if [ "$ALG" == "none" ]; then
echo "KRITIK: alg:none kullaniyor! Guvenli degil!"
fi
if [ "$ALG" == "HS256" ]; then
echo "UYARI: Simetrik algoritma. Microservice mimarisinde risk olusturabilir."
fi
# Expiry kontrolu
EXP=$(echo $PAYLOAD | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('exp','MISSING'))")
echo "Expiry: $EXP"
if [ "$EXP" == "MISSING" ]; then
echo "KRITIK: exp claim yok! Token suresiz gecerli!"
fi
CURRENT_TIME=$(date +%s)
if [ "$EXP" != "MISSING" ] && [ "$EXP" -lt "$CURRENT_TIME" ]; then
echo "Token suresi dolmus."
fi
SCRIPT
chmod +x /usr/local/bin/audit_jwt.sh
Monitoring ve Alerting
JWT güvenliğini sadece implementasyon aşamasında değil, sürekli izlemeniz gerekir.
# Fail2ban ile başarısız JWT doğrulama girişimlerini engelleme
cat << 'EOF' > /etc/fail2ban/filter.d/jwt-auth.conf
[Definition]
failregex = JWT validation failed.*<HOST>
Invalid token signature.*<HOST>
Token expired.*<HOST>
ignoreregex =
EOF
cat << 'EOF' > /etc/fail2ban/jail.d/jwt-auth.conf
[jwt-auth]
enabled = true
port = http,https
filter = jwt-auth
logpath = /var/log/nginx/api_error.log
maxretry = 10
findtime = 300
bantime = 3600
EOF
systemctl restart fail2ban
# Prometheus ile JWT metric'leri için log parsing
cat << 'EOF' > /etc/prometheus/rules/jwt_alerts.yml
groups:
- name: jwt_security
rules:
- alert: HighJWTFailureRate
expr: rate(jwt_validation_failures_total[5m]) > 0.1
for: 2m
labels:
severity: warning
annotations:
summary: "JWT dogrulama hata orani yuksek"
description: "Son 5 dakikada JWT hatasi: {{ $value }} req/s"
EOF
Sonuç
JWT güvenliği tek bir kararla biten bir konu değil, katmanlı bir yaklaşım gerektiriyor. Özetlemek gerekirse:
- İmzalama gizleme sağlamaz: Payload her zaman okunabilir, hassas veriyi oraya koymayın.
- JWE kullanın: Gerçekten gizli veri taşımanız gerekiyorsa JWE zorunlu.
- Asimetrik algoritmalar tercih edin: Microservice mimarisinde RS256 veya ES256 daha güvenli.
- Algorithm confusion’a karşı korunun: Kütüphanenizde izin verilen algoritmaları açıkça belirtin.
- Key yönetimine önem verin: Vault veya benzeri bir secret manager kullanın.
- Token süresini kısa tutun: Kısa access token, uzun refresh token stratejisi uygulayın.
- JWKS endpoint kullanın: Key rotation’ı kolaylaştırır ve otomatikleştirir.
- Sürekli izleyin: Anormal JWT doğrulama hataları bir saldırının işareti olabilir.
Bu konular ilk bakışta karmaşık görünse de bir kez doğru altyapıyı kurduğunuzda yönetimi oldukça kolaylaşıyor. Sorularınız varsa yorumlarda buluşalım.
