JWT Doğrulama: Nginx API Gateway Entegrasyonu
API gateway’inizi JWT ile korumak, modern mikroservis mimarilerinde artık bir lüks değil zorunluluk haline geldi. Nginx’in güçlü modül ekosistemi sayesinde, uygulama katmanınıza hiç dokunmadan token doğrulamayı merkezi bir noktada yapabilirsiniz. Bu yazıda, gerçek dünya senaryoları üzerinden Nginx’i bir JWT doğrulama katmanı olarak nasıl yapılandıracağınızı adım adım anlatacağım.
JWT Nedir ve Neden Nginx Katmanında Doğrulayalım?
JWT (JSON Web Token), üç bölümden oluşan bir token standardıdır: header, payload ve signature. Kullanıcı kimlik bilgilerini ve yetkilendirme verilerini güvenli bir şekilde taşır. Standart bir JWT şöyle görünür:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0IiwibmFtZSI6IkFobWV0IiwiaWF0IjoxNjE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Uygulamanızın her mikroservisine JWT doğrulama kodu yazmak yerine bunu Nginx’te merkezi olarak yapmak birkaç kritik avantaj sağlar:
- Tek sorumluluk: Her servis kendi işine odaklanır, auth mantığı gateway’de kalır
- Performans: Geçersiz tokenlar backend servislerine hiç ulaşmaz
- Tutarlılık: Tüm servislerde aynı doğrulama mantığı işler
- Kolay yönetim: Auth politikasını değiştirmek için tek nokta
Nginx’te JWT doğrulaması iki farklı yöntemle yapılabilir: nginx-plus (ticari) ile gelen native JWT modülü veya lua-nginx-module / ngx_http_auth_jwt_module gibi açık kaynak çözümler. Bu yazıda önce libnginx-mod-http-auth-jwt modülünü, sonra Lua tabanlı yaklaşımı ele alacağız.
Ortam Hazırlığı
Ubuntu 22.04 üzerinde çalışacağız. Önce gerekli paketleri kuralım:
# Nginx ve JWT modülü kurulumu
sudo apt update
sudo apt install -y nginx libnginx-mod-http-auth-jwt
# Modülün yüklendiğini doğrula
nginx -V 2>&1 | grep -o 'auth_jwt'
# Alternatif: OpenResty (Lua desteği için)
sudo apt install -y openresty
# Gerekli Lua kütüphanesi
sudo apt install -y lua-resty-jwt
JWT imzalama için kullanacağımız secret key’i ve test tokenlarını oluşturalım:
# HS256 için secret key oluştur
openssl rand -base64 32 | tr -d 'n' > /etc/nginx/jwt_secret.key
chmod 600 /etc/nginx/jwt_secret.key
# Test amaçlı JWT token oluştur (Python ile)
python3 -c "
import jwt
import time
secret = open('/etc/nginx/jwt_secret.key').read().strip()
payload = {
'sub': '1234567890',
'name': 'Ahmet Yilmaz',
'role': 'admin',
'iat': int(time.time()),
'exp': int(time.time()) + 3600
}
token = jwt.encode(payload, secret, algorithm='HS256')
print('Bearer ' + token)
"
Temel JWT Doğrulama Yapılandırması
Nginx JWT modülü kurulduktan sonra temel bir yapılandırma oluşturalım. Bu yapılandırmada /api/ altındaki tüm istekler için JWT doğrulaması zorunlu olacak:
# /etc/nginx/conf.d/api-gateway.conf
# JWT secret key'i tanımla
# Modül HS256 için symmetric key kullanır
map $http_authorization $jwt_token {
~^Bearers+(.+)$ $1;
default "";
}
server {
listen 80;
listen 443 ssl;
server_name api.sirketim.com;
ssl_certificate /etc/ssl/certs/api.sirketim.com.crt;
ssl_certificate_key /etc/ssl/private/api.sirketim.com.key;
# JWT doğrulama için secret key dosyası
auth_jwt_key_file /etc/nginx/jwt_secret.key;
# /api/ altındaki tüm endpointler korunsun
location /api/ {
auth_jwt "API Gateway" token=$jwt_token;
auth_jwt_leeway 30s; # Clock skew toleransı
# Doğrulanan claim'leri upstream'e header olarak ilet
proxy_set_header X-JWT-Sub $jwt_claim_sub;
proxy_set_header X-JWT-Role $jwt_claim_role;
proxy_set_header X-JWT-Name $jwt_claim_name;
proxy_pass http://backend_services;
}
# Health check endpoint - JWT gerektirmez
location /health {
auth_jwt off;
return 200 'OK';
add_header Content-Type text/plain;
}
# 401 hata sayfası
error_page 401 @unauthorized;
location @unauthorized {
add_header WWW-Authenticate 'Bearer realm="API Gateway"' always;
return 401 '{"error": "Unauthorized", "message": "Gecersiz veya eksik JWT token"}';
}
}
upstream backend_services {
server 127.0.0.1:3000;
server 127.0.0.1:3001 backup;
keepalive 32;
}
Yapılandırmayı test edip yeniden yükleyelim:
# Syntax kontrolü
nginx -t
# Yeniden yükle (bağlantıları kesmeden)
systemctl reload nginx
# Test isteği gönder
TOKEN="Bearer eyJhbGciOiJIUzI1NiJ9..."
# Geçerli token ile
curl -H "Authorization: $TOKEN" https://api.sirketim.com/api/users
# Tokensiz istek (401 dönmeli)
curl -v https://api.sirketim.com/api/users
Gelişmiş Claim Doğrulama
Sadece token’ın geçerli olması yeterli değil çoğu zaman. Belirli claim değerlerini de kontrol etmeniz gerekebilir. Örneğin sadece admin rolüne sahip kullanıcıların /api/admin/ endpointlerine erişmesine izin vermek:
# /etc/nginx/conf.d/api-gateway-advanced.conf
server {
listen 443 ssl;
server_name api.sirketim.com;
auth_jwt_key_file /etc/nginx/jwt_secret.key;
# Admin endpoint'leri - rol kontrolü
location /api/admin/ {
auth_jwt "Admin API";
# 'role' claim'i 'admin' olmalı
auth_jwt_require $jwt_claim_role admin;
# 'iss' (issuer) kontrolü
auth_jwt_require $jwt_claim_iss https://auth.sirketim.com;
proxy_pass http://admin_backend/;
proxy_set_header X-User-ID $jwt_claim_sub;
proxy_set_header X-User-Role $jwt_claim_role;
}
# Kullanıcı API'si - sadece geçerli token yeterli
location /api/user/ {
auth_jwt "User API";
proxy_pass http://user_backend/;
proxy_set_header X-User-ID $jwt_claim_sub;
}
# Okuma izni olan herkes erişebilir
location /api/public/ {
auth_jwt "Public API";
auth_jwt_require $jwt_claim_scope ~read;
proxy_pass http://public_backend/;
}
# Webhook endpoint'i - farklı bir key ile imzalanmış
location /webhooks/ {
auth_jwt_key_file /etc/nginx/webhook_secret.key;
auth_jwt "Webhook";
proxy_pass http://webhook_handler/;
}
}
Lua ile Özel JWT Doğrulama Mantığı
Bazen standart modülün yetenekleri yetmez. Veritabanı kontrolü, token blacklist doğrulaması veya özel iş mantığı için Lua’ya ihtiyaç duyarsınız. OpenResty kullanarak daha esnek bir yapı kuralım:
# /etc/openresty/conf.d/jwt-auth.conf
lua_package_path '/usr/local/lib/lua/?.lua;;';
lua_shared_dict jwt_cache 10m;
lua_shared_dict token_blacklist 5m;
# JWT doğrulama için inline Lua kodu
init_by_lua_block {
jwt = require("resty.jwt")
cjson = require("cjson")
}
server {
listen 443 ssl;
server_name api.sirketim.com;
location /api/ {
access_by_lua_block {
local function verify_jwt()
-- Authorization header'ı al
local auth_header = ngx.var.http_authorization
if not auth_header then
return false, "Authorization header eksik"
end
-- Bearer token'i ayikla
local token = auth_header:match("^Bearer%s+(.+)$")
if not token then
return false, "Gecersiz Authorization format"
end
-- Blacklist kontrolü
local blacklist = ngx.shared.token_blacklist
if blacklist:get(token) then
return false, "Token iptal edilmis"
end
-- Cache kontrolü (performans icin)
local cache = ngx.shared.jwt_cache
local cached = cache:get(token)
if cached then
local data = cjson.decode(cached)
ngx.req.set_header("X-User-ID", data.sub)
ngx.req.set_header("X-User-Role", data.role)
return true
end
-- Token dogrulama
local secret = io.open("/etc/nginx/jwt_secret.key"):read("*all")
secret = secret:gsub("%s+$", "")
local verified = jwt:verify(secret, token)
if not verified.verified then
return false, verified.reason
end
-- Expiry kontrolü
local payload = verified.payload
if payload.exp and payload.exp < ngx.time() then
return false, "Token suresi dolmus"
end
-- Issuer kontrolü
if payload.iss ~= "https://auth.sirketim.com" then
return false, "Gecersiz token issuer"
end
-- Cache'e kaydet (60 saniye)
cache:set(token, cjson.encode({
sub = payload.sub,
role = payload.role or "user"
}), 60)
-- Upstream'e bilgi ilet
ngx.req.set_header("X-User-ID", payload.sub)
ngx.req.set_header("X-User-Role", payload.role or "user")
ngx.req.set_header("X-Token-Exp", payload.exp)
return true
end
local ok, err = verify_jwt()
if not ok then
ngx.status = 401
ngx.header["Content-Type"] = "application/json"
ngx.header["WWW-Authenticate"] = 'Bearer realm="API"'
ngx.say(cjson.encode({
error = "Unauthorized",
message = err
}))
ngx.exit(401)
end
}
proxy_pass http://backend_services;
proxy_http_version 1.1;
proxy_set_header Connection "";
}
}
RS256 (Asimetrik) JWT Doğrulama
Production ortamlarında simetrik (HS256) yerine asimetrik algoritmalar (RS256) tercih edilir. Bu sayede public key ile doğrulama yaparken private key sadece auth servisinde kalır:
# RSA key pair oluştur
openssl genrsa -out /etc/nginx/jwt_private.pem 2048
openssl rsa -in /etc/nginx/jwt_private.pem -pubout -out /etc/nginx/jwt_public.pem
chmod 600 /etc/nginx/jwt_private.pem
chmod 644 /etc/nginx/jwt_public.pem
# Public key'i JWKS formatına çevir (opsiyonel)
python3 -c "
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.backends import default_backend
import base64, json, struct
with open('/etc/nginx/jwt_public.pem', 'rb') as f:
pub_key = serialization.load_pem_public_key(f.read(), backend=default_backend())
pub_numbers = pub_key.public_key().public_numbers()
def int_to_base64url(n):
length = (n.bit_length() + 7) // 8
return base64.urlsafe_b64encode(n.to_bytes(length, 'big')).rstrip(b'=').decode()
jwks = {
'keys': [{
'kty': 'RSA',
'use': 'sig',
'alg': 'RS256',
'kid': 'nginx-gateway-key-1',
'n': int_to_base64url(pub_numbers.n),
'e': int_to_base64url(pub_numbers.e)
}]
}
print(json.dumps(jwks, indent=2))
" > /etc/nginx/jwks.json
# RS256 için Nginx yapılandırması
server {
listen 443 ssl;
server_name api.sirketim.com;
# Public key ile dogrulama (RS256)
auth_jwt_key_file /etc/nginx/jwt_public.pem;
location /api/ {
auth_jwt "RS256 Protected API";
# Algoritma kontrolü (sadece RS256 kabul et)
# Bu ayar nginx-plus'ta direkt mevcut
# Açık kaynak için Lua ile yapılabilir
proxy_pass http://backend;
proxy_set_header X-User-ID $jwt_claim_sub;
}
# JWKS endpoint'i (diğer servislerin public key alması için)
location /.well-known/jwks.json {
auth_jwt off;
alias /etc/nginx/jwks.json;
add_header Content-Type application/json;
add_header Cache-Control "public, max-age=3600";
}
}
Rate Limiting ve JWT Entegrasyonu
JWT doğrulamasını rate limiting ile birleştirmek, hem güvenlik hem de kaynak yönetimi açısından kritik:
# /etc/nginx/conf.d/rate-limiting.conf
# JWT subject'e göre rate limit (kullanıcı bazlı)
limit_req_zone $jwt_claim_sub zone=per_user:10m rate=100r/m;
# Role'e göre farklı limit
map $jwt_claim_role $rate_limit_key {
admin ""; # Admin'lere limit yok
premium $jwt_claim_sub; # Premium kullanıcılar
default $jwt_claim_sub; # Normal kullanıcılar
}
limit_req_zone $rate_limit_key zone=api_limit:20m rate=60r/m;
server {
listen 443 ssl;
server_name api.sirketim.com;
auth_jwt_key_file /etc/nginx/jwt_secret.key;
location /api/ {
auth_jwt "API Gateway";
# Role bazlı rate limit uygula
limit_req zone=api_limit burst=20 nodelay;
limit_req_status 429;
# Rate limit aşıldığında özel mesaj
error_page 429 @rate_limited;
proxy_pass http://backend;
proxy_set_header X-User-ID $jwt_claim_sub;
proxy_set_header X-User-Role $jwt_claim_role;
# Response header'a kalan limit bilgisi ekle
add_header X-RateLimit-Limit 60 always;
add_header X-RateLimit-Remaining $upstream_http_x_ratelimit_remaining always;
}
location @rate_limited {
add_header Content-Type application/json always;
add_header Retry-After 60 always;
return 429 '{"error": "Too Many Requests", "message": "Rate limit asimina ugradiniz, 60 saniye sonra tekrar deneyin"}';
}
}
Token Yenileme ve Geçici Erişim Senaryosu
Gerçek dünya senaryosu: Bir e-ticaret platformunda kullanıcı token’ı süresi dolduğunda sessizce yenilensin, ama kritik işlemler (ödeme, şifre değişikliği) her zaman fresh token gerektirsin:
# /etc/nginx/conf.d/ecommerce-api.conf
map $jwt_claim_iat $token_age {
~^(.+)$ $1;
default 0;
}
server {
listen 443 ssl;
server_name api.eticaret.com;
auth_jwt_key_file /etc/nginx/jwt_secret.key;
# Kritik islemler - maksimum 5 dakika onceki token
location ~ ^/api/(payment|account/password|account/email) {
auth_jwt "Critical Operations";
access_by_lua_block {
local iat = ngx.var.jwt_claim_iat
if not iat or (ngx.time() - tonumber(iat)) > 300 then
ngx.status = 401
ngx.header["Content-Type"] = "application/json"
ngx.header["X-Token-Refresh-Required"] = "true"
ngx.say('{"error": "Fresh token required", "refresh": true}')
ngx.exit(401)
end
}
proxy_pass http://payment_service;
proxy_set_header X-User-ID $jwt_claim_sub;
}
# Normal islemler - standart token suresi yeterli
location /api/ {
auth_jwt "Standard API";
# Token suresi dolmak uzere ise header ekle
header_filter_by_lua_block {
local exp = ngx.var.jwt_claim_exp
if exp then
local remaining = tonumber(exp) - ngx.time()
if remaining < 300 then -- 5 dakikadan az kaldiysa
ngx.header["X-Token-Expires-In"] = remaining
ngx.header["X-Token-Refresh-Recommended"] = "true"
end
end
}
proxy_pass http://main_backend;
proxy_set_header X-User-ID $jwt_claim_sub;
proxy_set_header X-User-Tier $jwt_claim_tier;
}
}
Log ve Monitoring Yapılandırması
JWT doğrulama işlemlerini izlemek için özel log formatı oluşturun:
# /etc/nginx/nginx.conf içinde log format tanımı
cat >> /etc/nginx/nginx.conf << 'EOF'
log_format jwt_audit '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'jwt_sub="$jwt_claim_sub" '
'jwt_role="$jwt_claim_role" '
'jwt_exp="$jwt_claim_exp" '
'response_time="$upstream_response_time"';
EOF
# Log rotation ayarı
cat > /etc/logrotate.d/nginx-jwt << 'EOF'
/var/log/nginx/jwt_access.log {
daily
rotate 30
compress
delaycompress
missingok
notifempty
sharedscripts
postrotate
nginx -s reopen
endscript
}
EOF
# Gerçek zamanlı JWT hata takibi
tail -f /var/log/nginx/error.log | grep -E "(JWT|jwt|401|403)"
# Başarısız doğrulama istatistikleri
awk '/jwt/ && /401/' /var/log/nginx/jwt_access.log |
awk '{print $14}' | sort | uniq -c | sort -rn | head 20
Yaygın Sorunlar ve Çözümleri
Production’da karşılaşılan en sık problemler:
- Clock skew sorunu: Sunucu saatleri arasındaki fark token’ın geçersiz görünmesine neden olabilir.
auth_jwt_leeway 30sveya NTP senkronizasyonu (timedatectl set-ntp true) ile çözün.
- Büyük payload sorunu: JWT payload’ı çok büyükse Nginx header buffer’larını artırın.
large_client_header_buffers 4 32kveproxy_buffer_size 32kayarlarını kontrol edin.
- CORS ve JWT birlikte kullanımı: OPTIONS isteklerinin JWT doğrulamasını atlaması gerekir.
if ($request_method = OPTIONS) { auth_jwt off; }yerine ayrı bir location bloğu kullanın.
- Token’daki özel karakterler: Base64url encoding’de
+,/,=yerine-,_kullanılır. Nginx regex’lerinde bu karakterleri doğru handle edin.
- Modül yüklenmeme sorunu:
nginx -Vçıktısında modülü göremiyorsanız,load_module modules/ngx_http_auth_jwt_module.so;satırınınginx.conf‘un en üstüne ekleyin.
Sonuç
Nginx’i JWT doğrulama katmanı olarak kullanmak, API güvenliği mimarinizi önemli ölçüde sadeleştirir. Temel HS256 doğrulamasından RS256 asimetrik şifrelemeye, Lua ile özelleştirilmiş iş mantığından kullanıcı bazlı rate limiting’e kadar ele aldığımız yapılandırmalar, büyük çoğunluğunun ihtiyaçlarını karşılar.
Özellikle dikkat etmeniz gereken nokta şu: JWT doğrulaması tek başına yeterli değil. Bunu TLS zorunluluğu, rate limiting, IP filtreleme ve audit logging ile birleştirdiğinizde gerçekten sağlam bir API gateway elde edersiniz. Token blacklist yönetimi için Redis gibi hızlı bir backend eklemek de production hazırlığınızı tamamlar.
Son olarak, tüm bu yapılandırmaları version control’e alın ve infrastructure as code yaklaşımıyla yönetin. Bir gün JWT secret’ınızı rotate etmeniz gerektiğinde, Ansible veya Terraform ile bunu birkaç dakikada halledebilmek hayat kurtarır.
