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 30s veya 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 32k ve proxy_buffer_size 32k ayarları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.

Bir yanıt yazın

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