Solution française • Hébergement souverain • Conformité européenne Blog IA souveraine

API Gateway sécurisée pour LLM — Kong vs NGINX vs Traefik en production

Exposer un LLM directement sur le réseau, même interne, est une erreur architecturale grave. Sans API Gateway, n'importe quel utilisateur peut soumettre des requêtes illimitées, saturer la VRAM GPU, exfiltrer des données via des prompts malicieux, et rendre l'audit impossible. Une API Gateway est le premier niveau de défense d'une plateforme LLM souveraine — autant dans sa configuration que dans son choix technologique.

Pourquoi une API Gateway est indispensable devant un LLM

Un LLM exposé sans API Gateway est comparable à une base de données exposée directement sur Internet sans firewall applicatif. Les risques sont multiples et souvent sous-estimés par les équipes qui ont l'habitude de déployer des APIs REST classiques.

SANS API GATEWAY (dangereux) :
Utilisateurs → [vLLM :8000] → GPU

Problèmes :
• Aucun rate limiting → saturation GPU possible en minutes
• Aucune authentification → accès anonyme
• Aucun audit → impossible de savoir qui demande quoi
• Aucun filtrage → prompt injection non détectée
• Aucun cost tracking → pas de chargeback possible

AVEC API GATEWAY (production) :
Utilisateurs → [Kong/NGINX] → Auth → Rate Limit → Audit → [vLLM] → GPU
                                ↓
                          Prometheus metrics
                          Audit logs (SIEM)
                          Cost tracking DB
100%Requêtes auditées et traçables
×10Réduction saturation GPU avec rate limiting
3 msOverhead latence Kong (median)
OWASPLLM01-LLM10 mitigés en gateway

Comparatif Kong vs NGINX Plus vs Traefik vs AWS API Gateway

CritèreKong OSS/EENGINX PlusTraefik EEAWS API GW
LicenceApache 2.0 (OSS) / CommercialCommercialMIT (OSS) / CommercialSaaS AWS
HébergementOn-premise / K8sOn-premise / K8sOn-premise / K8sCloud AWS uniquement
Plugin LLM natifOui (AI Gateway 3.6+)Non (custom Lua/NJS)Non (middleware custom)Partiel (Bedrock)
Rate limitingPar user/IP/service/groupePar zone (limit_req)Par IP / headerPar API Key / plan
JWT authPlugin natifModule ngx_http_auth_jwtMiddleware natifAuthorizer Lambda
Streaming SSEOui (pass-through)Oui (proxy_buffering off)OuiLimitations (timeout 29s)
Performance (RPS)50-100k RPS100-200k RPS30-80k RPS10k RPS (configurable)
Cost tracking tokensPlugin AI Cost (EE) / CustomCustom NJSMiddleware customNon
Souveraineté donnéesTotale (on-prem)Totale (on-prem)Totale (on-prem)Données transitent AWS
Courbe apprentissageMoyenneÉlevée (config NGINX)Faible (YAML/labels K8s)Faible (console AWS)

Recommandation Intelligence Privée : Kong OSS avec des plugins custom est le choix optimal pour un LLM souverain on-premise. Il combine un plugin AI Gateway natif (Kong 3.6+), une excellente intégration Kubernetes, et une communauté active. NGINX Plus est une alternative viable si votre équipe a déjà des compétences NGINX profondes.

Rate limiting par utilisateur et département

Le rate limiting LLM diffère du rate limiting classique : on ne compte pas seulement les requêtes, mais aussi les tokens consommés. Une seule requête de 32k tokens peut peser autant que 100 requêtes standard.

# kong-rate-limiting-plugin.yaml
# Stratégie : rate limiting sur 3 niveaux

# Niveau 1 : Global (protection anti-DDoS)
_format_version: "3.0"
plugins:
- name: rate-limiting-advanced
  config:
    limit:
    - 10000  # 10k requêtes/minute global
    window_size:
    - 60
    identifier: ip
    strategy: redis
    redis:
      host: redis.cache
      port: 6379
      ssl: true

# Niveau 2 : Par utilisateur authentifié (JWT sub)
- name: rate-limiting-advanced
  route: llm-chat-route
  config:
    limit:
    - 50   # 50 requêtes/minute par user
    - 500  # 500 requêtes/heure par user
    window_size:
    - 60
    - 3600
    identifier: consumer
    strategy: redis

# Niveau 3 : Par département (Consumer Group)
- name: rate-limiting-advanced
  consumer_group: finance-department
  config:
    limit:
    - 200   # 200 req/min pour le département Finance
    - 2000  # 2000 req/heure
    window_size:
    - 60
    - 3600
    identifier: consumer
    strategy: redis
    error_message: "Quota département Finance atteint. Contactez votre DSI."

Rate limiting sur les tokens (approche avancée)

# middleware_token_counter.py — Kong custom plugin (Python)
# Compte les tokens dans la réponse LLM et applique un quota

import json
from kong_pdk import Plugin

class TokenRateLimiter(Plugin):
    def access(self, kong):
        # Lire les headers de la requête
        consumer_id = kong.client.get_consumer().get('id', 'anonymous')
        
        # Vérifier le quota tokens restant depuis Redis
        used_tokens = self.redis_get(f"tokens:{consumer_id}:hourly")
        quota = self.get_quota(consumer_id)  # Ex: 100000 tokens/heure
        
        if used_tokens and int(used_tokens) >= quota:
            return kong.response.exit(429, {
                "error": "Token quota exceeded",
                "used": used_tokens,
                "quota": quota,
                "reset_in": self.get_ttl(f"tokens:{consumer_id}:hourly")
            })

    def response(self, kong):
        # Lire la réponse vLLM
        body = kong.response.get_raw_body()
        try:
            data = json.loads(body)
            tokens_used = data.get('usage', {}).get('total_tokens', 0)
            consumer_id = kong.client.get_consumer().get('id')
            
            # Incrémenter le compteur tokens
            self.redis_incrby(
                f"tokens:{consumer_id}:hourly",
                tokens_used,
                expire=3600
            )
            
            # Ajouter headers informatifs
            kong.response.set_header('X-Tokens-Used', str(tokens_used))
            kong.response.set_header('X-Tokens-Quota-Remaining',
                str(self.get_quota(consumer_id) - tokens_used))
        except:
            pass

Authentification JWT + API Keys

Une plateforme LLM d'entreprise doit supporter deux modes d'authentification : JWT (pour les utilisateurs humains via SSO Keycloak) et API Keys (pour les services et applications machine-to-machine).

# kong-auth-config.yaml

# Plugin JWT — validation des tokens Keycloak
plugins:
- name: jwt
  route: llm-api-route
  config:
    uri_param_names:
    - jwt
    header_names:
    - authorization
    secret_is_base64: false
    claims_to_verify:
    - exp
    - nbf
    key_claim_name: iss
    anonymous: null  # Aucun accès anonyme
    run_on_preflight: false

# Plugin Key Auth — pour les intégrations machine-to-machine
- name: key-auth
  route: llm-api-m2m-route
  config:
    key_names:
    - apikey
    - X-API-Key
    hide_credentials: true  # Ne pas transmettre la clé au backend
    anonymous: null
    key_in_header: true
    key_in_query: false  # Désactiver query param (risque logs)
    key_in_body: false
# Création d'un consumer Kong avec JWT + API Key
# 1. Créer le consumer
curl -X POST http://kong-admin:8001/consumers \
  -H 'Content-Type: application/json' \
  -d '{"username": "service-crm", "custom_id": "crm-v2"}'

# 2. Associer au groupe département
curl -X POST http://kong-admin:8001/consumers/service-crm/consumer_groups \
  -d '{"group": "it-department"}'

# 3. Créer une API Key
curl -X POST http://kong-admin:8001/consumers/service-crm/key-auth \
  -d '{"key": "'$(openssl rand -hex 32)'"}'

# 4. Configurer les claims JWT acceptés (Keycloak JWKS)
curl -X POST http://kong-admin:8001/consumers/service-crm/jwt \
  -d 'algorithm=RS256' \
  -d 'rsa_public_key=@/certs/keycloak-public.pem' \
  -d 'key=https://keycloak.interne/realms/intelligence-privee'

Audit logs structurés

L'audit complet des interactions LLM est une exigence réglementaire (RGPD Article 30, NIS2, secteurs financiers) et opérationnelle. Chaque requête doit être loggée avec : identité de l'utilisateur, horodatage, tokens consommés, modèle utilisé, et durée.

# kong-file-log-plugin.yaml — Logging structuré JSON
plugins:
- name: file-log
  route: llm-api-route
  config:
    path: /var/log/kong/llm-audit.log
    reopen: true
    custom_fields_by_lua:
      llm_model: "return kong.request.get_header('x-model-id') or 'unknown'"
      department: "return kong.client.get_consumer() and kong.client.get_consumer().custom_id or 'unknown'"
      prompt_tokens: "return kong.response.get_header('x-prompt-tokens') or '0'"
      completion_tokens: "return kong.response.get_header('x-completion-tokens') or '0'"
      request_id: "return kong.request.get_header('x-request-id') or kong.request.get_id()"

Exemple de log structuré généré :

{
  "timestamp": "2026-02-10T14:32:17.421Z",
  "request_id": "a3f7e891-4b2c-4d1e-8f3a-12345678",
  "consumer": {
    "username": "jean.dupont@entreprise.fr",
    "custom_id": "finance-department"
  },
  "request": {
    "method": "POST",
    "uri": "/v1/chat/completions",
    "size": 1247
  },
  "response": {
    "status": 200,
    "latency": 1832,
    "size": 3891
  },
  "llm_model": "elodie-32b",
  "department": "finance-department",
  "prompt_tokens": 312,
  "completion_tokens": 847,
  "total_tokens": 1159,
  "service": {
    "name": "vllm-elodie",
    "host": "vllm-service.llm-production"
  }
}

Prompt filtering middleware

Le filtrage des prompts à la Gateway permet de bloquer les attaques avant qu'elles n'atteignent le LLM. Trois types de filtres sont indispensables en production.

# prompt_security_filter.py — Kong custom plugin
import re
import json

# Patterns de prompt injection connus
INJECTION_PATTERNS = [
    r'ignore (all |the |previous )?(instructions|prompt|context)',
    r'\\n\\nHuman:',
    r'<\|im_start\|>system',
    r'SYSTEM:.*OVERRIDE',
    r'\[INST\].*\[/INST\]',  # Template Llama injection
    r'Act as (DAN|AIM|STAN|DUDE)',
    r'Pretend (you are|you\'re) (not |un)?restricted',
    r'jailbreak',
    r'Do Anything Now'
]

# Patterns PII à détecter (avertissement RSSI)
PII_PATTERNS = [
    r'\b[A-Z]{2}[0-9]{2}[ ][A-Z0-9]{4}[ ][0-9]{4}[ ][0-9]{4}[ ][0-9]{4}[ ][0-9]{4}[ ][0-9]{2}\b',  # IBAN
    r'\b[0-9]{13,16}\b',  # Numéros de carte bancaire potentiels
    r'\b[0-9]{15}\b',     # Numéros SS français
]

def filter_prompt(body: dict) -> tuple[bool, str]:
    """Retourne (should_block, reason)"""
    messages = body.get('messages', [])
    full_text = ' '.join(
        msg.get('content', '') for msg in messages
        if isinstance(msg.get('content'), str)
    )
    
    # Check injection
    for pattern in INJECTION_PATTERNS:
        if re.search(pattern, full_text, re.IGNORECASE):
            return True, f"Prompt injection pattern detected: {pattern[:30]}"
    
    # Check longueur excessive (protection contre context stuffing)
    if len(full_text) > 50000:  # ~12k tokens
        return True, "Prompt exceeds maximum allowed length"
    
    # Check PII (log warning, ne pas bloquer)
    for pattern in PII_PATTERNS:
        if re.search(pattern, full_text):
            # Logger en SIEM mais ne pas bloquer
            log_pii_warning(full_text[:100])
            break
    
    return False, ""

Cost tracking par équipe

# cost_tracker.py — Calcul du coût par requête
# Intégré comme post-processing dans Kong via HTTP Log plugin

from datetime import datetime
import psycopg2

# Tarif interne : coût GPU amorti par token
COST_PER_1K_TOKENS = {
    "elodie-32b": 0.004,   # 0.004€ per 1k tokens (basé sur TCO GPU)
    "kevina-32b": 0.004,
    "llama-70b": 0.012,    # Modèle plus lourd = plus cher
}

def record_usage(log_entry: dict):
    model = log_entry.get('llm_model', 'unknown')
    total_tokens = int(log_entry.get('total_tokens', 0))
    department = log_entry.get('department', 'unknown')
    user = log_entry.get('consumer', {}).get('username', 'unknown')
    
    cost_per_token = COST_PER_1K_TOKENS.get(model, 0.005) / 1000
    cost_eur = total_tokens * cost_per_token
    
    # Insérer dans PostgreSQL pour reporting mensuel
    conn = psycopg2.connect("postgresql://llm_tracker:secret@postgres:5432/llm_costs")
    cur = conn.cursor()
    cur.execute("""
        INSERT INTO llm_usage 
        (timestamp, user_email, department, model, tokens_used, cost_eur, request_id)
        VALUES (%s, %s, %s, %s, %s, %s, %s)
    """, (
        datetime.utcnow(),
        user, department, model,
        total_tokens, cost_eur,
        log_entry.get('request_id')
    ))
    conn.commit()

# Requête de chargeback mensuel par département
MONTHLY_CHARGEBACK_QUERY = """
SELECT 
    department,
    COUNT(*) as request_count,
    SUM(tokens_used) as total_tokens,
    ROUND(SUM(cost_eur)::numeric, 2) as total_cost_eur,
    ROUND(AVG(cost_eur)::numeric, 4) as avg_cost_per_request
FROM llm_usage
WHERE timestamp >= date_trunc('month', CURRENT_DATE)
GROUP BY department
ORDER BY total_cost_eur DESC;
"""

Configuration Kong complète (Production)

# kong-production-config.yaml — Config déclarative complète
_format_version: "3.0"
_transform: true

services:
- name: vllm-elodie
  url: http://vllm-service.llm-production/v1
  connect_timeout: 5000
  write_timeout: 120000   # 2 minutes pour les longues générations
  read_timeout: 120000
  retries: 1
  routes:
  - name: llm-chat
    paths:
    - /v1/chat/completions
    - /v1/completions
    - /v1/models
    methods:
    - POST
    - GET
    strip_path: false
    preserve_host: false
    protocols:
    - https
    plugins:
    - name: jwt
      config:
        header_names: [authorization]
        claims_to_verify: [exp]
    - name: rate-limiting-advanced
      config:
        limit: [50, 1000]
        window_size: [60, 3600]
        identifier: consumer
        strategy: redis
        redis: {host: redis.cache, port: 6379}
    - name: request-size-limiting
      config:
        allowed_payload_size: 10  # 10 MB max
        size_unit: megabytes
    - name: response-ratelimiting
      config:
        limits:
          tokens_per_hour:
            hour: 100000  # 100k tokens/heure
    - name: http-log
      config:
        http_endpoint: http://log-collector:8888/llm-audit
        method: POST
        timeout: 1000
        keepalive: 60000
        flush_timeout: 2
    - name: correlation-id
      config:
        header_name: X-Request-ID
        generator: uuid
        echo_downstream: true
    - name: response-transformer
      config:
        add:
          headers:
          - "X-Served-By:intelligence-privee-llm"
          - "X-Model:elodie-32b"
          - "Strict-Transport-Security:max-age=31536000"

plugins:
- name: bot-detection
  config:
    allow: []
    deny: [googlebot, baiduspider, bingbot]  # Bloquer crawlers
- name: ip-restriction
  config:
    allow:
    - 10.0.0.0/8      # Réseau interne uniquement
    - 172.16.0.0/12
    - 192.168.0.0/16

Mitigations OWASP LLM Top 10 via Gateway

OWASP LLMRisqueMitigation Gateway
LLM01 - Prompt InjectionManipulation du comportement LLMRegex filtering, longueur max, blocklist patterns
LLM02 - Insecure OutputOutput dangereux non filtréResponse body inspection plugin
LLM03 - Training Data PoisoningDonnées d'entraînement corrompuesNon applicable (couche modèle)
LLM04 - Denial of ServiceSaturation GPU par requêtes massivesRate limiting multi-niveau + request size limiting
LLM06 - Sensitive Info DisclosureFuite de données sensiblesPII detection plugin + audit logs
LLM07 - Insecure Plugin DesignPlugins LLM malveillantsWhitelist d'outils autorisés dans le body
LLM09 - OverrelianceConfiance aveugle dans les outputsHeaders avertissement dans les réponses
LLM10 - Model TheftVol du modèle via APIRate limiting + détection d'extraction (pattern requêtes)

Ce qu'il faut retenir

  • Kong OSS 3.6+ avec le plugin AI Gateway est la solution la plus complète pour un LLM souverain on-premise : rate limiting par tokens, JWT natif, logging structuré.
  • Ne jamais exposer vLLM directement — même sur réseau interne. L'API Gateway est la première ligne de défense contre les abus internes et les prompt injections.
  • Le rate limiting doit opérer sur 3 niveaux : global (anti-DDoS), par utilisateur (équité), par département (budget).
  • Le cost tracking par tokens permet un chargeback réel et incite les équipes à optimiser leurs prompts.
  • Le prompt filtering par regex couvre 70-80% des attaques connues à coût zéro — complétez avec un modèle de classification pour les attaques sophistiquées.

Sécuriser votre infrastructure LLM

Nos experts configurent votre API Gateway LLM avec les plugins de sécurité adaptés à vos risques réels — rate limiting, audit RGPD, prompt filtering et cost tracking dès le premier jour.

Configurer ma Gateway LLM →

FAQ

Kong introduit-il une latence significative ?

Kong en mode DB-less (déclaratif) ajoute typiquement 2 à 5 ms de latence median. Avec les plugins JWT et rate-limiting Redis actifs, comptez 5 à 15 ms. Comparé aux latences de génération LLM (200ms à plusieurs secondes), c'est négligeable. Pour minimiser, déployez Kong sur le même cluster Kubernetes que vLLM (pas de sauts réseau additionnels) et utilisez Redis local (127.0.0.1) pour le rate limiting.

Le streaming SSE (Server-Sent Events) fonctionne-t-il à travers Kong ?

Oui. Kong passe les streams SSE en mode transparent. Assurez-vous que le plugin response-transformer ne bufferise pas la réponse complète (ce qui casserait le streaming). Configurez response_buffering: false sur la route Kong et vérifiez que le proxy upstream n'active pas proxy_buffering on. Le timeout read_timeout doit être suffisamment long (120 000 ms minimum pour les génération longues).

Comment détecter une tentative d'extraction du modèle via l'API ?

La méthode d'extraction la plus courante consiste à envoyer des milliers de requêtes avec des prompts semi-aléatoires pour reconstruire les poids par inférence. En pratique, cette attaque est facilement détectable : un utilisateur légitime ne fait pas 500 requêtes/heure avec des prompts de 1 token. Configurez une alerte Prometheus sur rate(kong_http_requests_total[1m]) > 100 par consumer et bloquez automatiquement via l'API Kong admin.