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
Comparatif Kong vs NGINX Plus vs Traefik vs AWS API Gateway
| Critère | Kong OSS/EE | NGINX Plus | Traefik EE | AWS API GW |
|---|---|---|---|---|
| Licence | Apache 2.0 (OSS) / Commercial | Commercial | MIT (OSS) / Commercial | SaaS AWS |
| Hébergement | On-premise / K8s | On-premise / K8s | On-premise / K8s | Cloud AWS uniquement |
| Plugin LLM natif | Oui (AI Gateway 3.6+) | Non (custom Lua/NJS) | Non (middleware custom) | Partiel (Bedrock) |
| Rate limiting | Par user/IP/service/groupe | Par zone (limit_req) | Par IP / header | Par API Key / plan |
| JWT auth | Plugin natif | Module ngx_http_auth_jwt | Middleware natif | Authorizer Lambda |
| Streaming SSE | Oui (pass-through) | Oui (proxy_buffering off) | Oui | Limitations (timeout 29s) |
| Performance (RPS) | 50-100k RPS | 100-200k RPS | 30-80k RPS | 10k RPS (configurable) |
| Cost tracking tokens | Plugin AI Cost (EE) / Custom | Custom NJS | Middleware custom | Non |
| Souveraineté données | Totale (on-prem) | Totale (on-prem) | Totale (on-prem) | Données transitent AWS |
| Courbe apprentissage | Moyenne | É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 LLM | Risque | Mitigation Gateway |
|---|---|---|
| LLM01 - Prompt Injection | Manipulation du comportement LLM | Regex filtering, longueur max, blocklist patterns |
| LLM02 - Insecure Output | Output dangereux non filtré | Response body inspection plugin |
| LLM03 - Training Data Poisoning | Données d'entraînement corrompues | Non applicable (couche modèle) |
| LLM04 - Denial of Service | Saturation GPU par requêtes massives | Rate limiting multi-niveau + request size limiting |
| LLM06 - Sensitive Info Disclosure | Fuite de données sensibles | PII detection plugin + audit logs |
| LLM07 - Insecure Plugin Design | Plugins LLM malveillants | Whitelist d'outils autorisés dans le body |
| LLM09 - Overreliance | Confiance aveugle dans les outputs | Headers avertissement dans les réponses |
| LLM10 - Model Theft | Vol du modèle via API | Rate 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.