Votre instance Odoo est à une URL d'un effacement complet de base de données
Par défaut, Odoo expose /web/database/manager à l'ensemble d'internet. Quiconque connaît cette URL et le mot de passe maître (qui est admin par défaut sur de nombreuses installations) peut créer, dupliquer, supprimer, sauvegarder et restaurer l'intégralité de votre base de données. Nous avons audité des installations Odoo où cet endpoint était actif en production depuis plus d'un an avec le mot de passe par défaut toujours en place.
Mais le gestionnaire de base de données n'est que la faille la plus évidente. Sans reverse proxy, Odoo manque également de : terminaison SSL/TLS (les sessions circulent en clair), rate limiting (les bots et attaques par force brute frappent le serveur à pleine vitesse), mise en cache des fichiers statiques (chaque requête CSS/JS passe par Python), et contrôle de la taille des requêtes (un seul upload malformé peut provoquer un OOM sur vos workers).
Nginx se place entre internet et Odoo, gérant tout ce que le serveur applicatif ne devrait pas gérer. Ce guide couvre une configuration Nginx complète et testée en production pour Odoo 19 — du proxy de base au durcissement sécurité, en passant par le rate limiting, le support WebSocket et l'optimisation des performances.
Configuration Nginx complète pour Odoo 19 avec SSL et support WebSocket
Voici la configuration Nginx complète que nous utilisons comme base pour chaque déploiement client. Elle gère la terminaison SSL, la redirection HTTP vers HTTPS, le proxying du backend Odoo, le support WebSocket (longpolling) et le transfert correct des en-têtes.
# ── Rate Limiting Zones (defined before server blocks) ──
# Shared memory zones: 10m ≈ 160,000 IP entries
limit_req_zone $binary_remote_addr zone=odoo_login:10m rate=5r/m;
limit_req_zone $binary_remote_addr zone=odoo_api:10m rate=30r/m;
limit_req_zone $binary_remote_addr zone=odoo_general:10m rate=10r/s;
# ── Connection limit per IP ──
limit_conn_zone $binary_remote_addr zone=addr:10m;
# ── Upstreams ──
upstream odoo_backend {
server 127.0.0.1:8069;
keepalive 32;
}
upstream odoo_longpoll {
server 127.0.0.1:8072;
keepalive 8;
}
# ── HTTP → HTTPS Redirect ──
server {
listen 80;
listen [::]:80;
server_name erp.yourcompany.com;
return 301 https://$host$request_uri;
}
# ── Main HTTPS Server ──
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name erp.yourcompany.com;
# ── SSL Configuration ──
ssl_certificate /etc/letsencrypt/live/erp.yourcompany.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/erp.yourcompany.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
# OCSP stapling
ssl_stapling on;
ssl_stapling_verify on;
resolver 1.1.1.1 8.8.8.8 valid=300s;
resolver_timeout 5s;
# ── Security Headers ──
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
# ── Request Limits ──
client_max_body_size 256m; # File uploads, data imports
client_body_timeout 60s;
client_header_timeout 60s;
# ── Connection Limits ──
limit_conn addr 50; # Max 50 connections per IP
# ── General Rate Limit ──
limit_req zone=odoo_general burst=20 nodelay;
# ── Proxy Defaults ──
proxy_read_timeout 720s;
proxy_connect_timeout 720s;
proxy_send_timeout 720s;
proxy_buffers 16 64k;
proxy_buffer_size 128k;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
# ═══════════════════════════════════════════════
# BLOCK 1: Dangerous Endpoints — Deny Entirely
# ═══════════════════════════════════════════════
# Database manager — the #1 attack vector
location ~* ^/web/database {
deny all;
return 404;
}
# Server info leaks
location = /web/webclient/version_info {
deny all;
return 404;
}
# Debug/profiling endpoints
location ~* ^/web/debug {
deny all;
return 404;
}
# ═══════════════════════════════════════════════
# BLOCK 2: Auth Endpoints — Strict Rate Limiting
# ═══════════════════════════════════════════════
# Login page — 5 attempts per minute per IP
location = /web/login {
limit_req zone=odoo_login burst=3 nodelay;
limit_req_status 429;
proxy_pass http://odoo_backend;
proxy_redirect off;
}
# JSON-RPC authentication
location = /web/session/authenticate {
limit_req zone=odoo_login burst=3 nodelay;
limit_req_status 429;
proxy_pass http://odoo_backend;
proxy_redirect off;
}
# Password reset
location = /web/reset_password {
limit_req zone=odoo_login burst=2 nodelay;
limit_req_status 429;
proxy_pass http://odoo_backend;
proxy_redirect off;
}
# ═══════════════════════════════════════════════
# BLOCK 3: API Endpoints — Moderate Rate Limiting
# ═══════════════════════════════════════════════
# JSON-RPC API (external integrations)
location = /jsonrpc {
limit_req zone=odoo_api burst=10 nodelay;
limit_req_status 429;
proxy_pass http://odoo_backend;
proxy_redirect off;
}
# XML-RPC (legacy integrations)
location ~ ^/xmlrpc/ {
limit_req zone=odoo_api burst=10 nodelay;
limit_req_status 429;
proxy_pass http://odoo_backend;
proxy_redirect off;
}
# ═══════════════════════════════════════════════
# BLOCK 4: WebSocket / Longpolling
# ═══════════════════════════════════════════════
location /websocket {
proxy_pass http://odoo_longpoll;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
# Legacy longpolling endpoint (Odoo < 17 compat)
location /longpolling {
proxy_pass http://odoo_longpoll;
}
# ═══════════════════════════════════════════════
# BLOCK 5: Static Assets — Aggressive Caching
# ═══════════════════════════════════════════════
location ~* /web/static/ {
proxy_pass http://odoo_backend;
proxy_cache_valid 200 365d;
expires 365d;
add_header Cache-Control "public, immutable";
access_log off;
}
location ~* /web/image/ {
proxy_pass http://odoo_backend;
proxy_cache_valid 200 30d;
expires 30d;
add_header Cache-Control "public";
access_log off;
}
# ═══════════════════════════════════════════════
# BLOCK 6: Everything Else — Default Proxy
# ═══════════════════════════════════════════════
location / {
proxy_pass http://odoo_backend;
proxy_redirect off;
}
}Sécuriser Odoo 19 derrière Nginx : bloquer le gestionnaire de base de données et les endpoints sensibles
La configuration ci-dessus bloque trois catégories d'URLs dangereuses. Analysons pourquoi chacune est importante et ce qui se passe sans ces protections :
| Endpoint | Ce qu'il expose | Vecteur d'attaque | Règle Nginx |
|---|---|---|---|
/web/database/* | Gestion complète de la BDD : créer, supprimer, sauvegarder, restaurer, dupliquer | Force brute du mot de passe maître → suppression de la BDD de production | deny all; return 404; |
/web/webclient/version_info | Version exacte d'Odoo, infos serveur, modules installés | Fingerprinting → cibler les CVE connues pour cette version exacte | deny all; return 404; |
/web/debug | Activation du mode debug, débogage des assets, profileur | Divulgation d'informations, dégradation des performances | deny all; return 404; |
/web/login | Endpoint d'authentification | Credential stuffing, force brute | limit_req 5r/m |
/jsonrpc / /xmlrpc | Surface API complète (CRUD sur tous les modèles) | Abus d'API, exfiltration de données via identifiants volés | limit_req 30r/m |
Un 403 Forbidden confirme que l'endpoint existe — il dit à un attaquant « vous avez trouvé quelque chose, essayez encore ». Un 404 Not Found ne révèle rien. L'endpoint semble ne pas exister. C'est de la sécurité par l'ambiguïté — ce n'est pas la seule défense, mais c'est une couche utile.
Whitelist IP pour les outils internes
Certaines équipes ont toujours besoin du gestionnaire de base de données pendant les fenêtres de maintenance. Au lieu de le laisser ouvert, autorisez uniquement l'IP de votre bureau/VPN :
# Allow database manager from office VPN only
location ~* ^/web/database {
allow 203.0.113.50; # Office static IP
allow 10.0.0.0/8; # Internal VPN range
deny all;
proxy_pass http://odoo_backend;
proxy_redirect off;
}Rate Limiting Nginx pour Odoo 19 : protéger les endpoints Login, API et RPC
Le rate limiting est votre première ligne de défense contre les attaques par force brute et l'abus d'API. Odoo n'a aucun rate limiting intégré — chaque tentative de connexion, chaque appel API et chaque requête RPC frappe le serveur applicatif à la vitesse que l'attaquant peut envoyer.
Fonctionnement des zones de rate limiting
| Zone | Débit | Burst | Endpoints protégés | Pourquoi ce débit |
|---|---|---|---|---|
odoo_login | 5 req/min | 3 | /web/login, /web/session/authenticate, /web/reset_password | Un humain tape un mot de passe en 2-3 secondes. 5/min permet une utilisation normale, bloque les outils automatisés qui font 1000/min. |
odoo_api | 30 req/min | 10 | /jsonrpc, /xmlrpc/* | Les intégrations légitimes (sync e-commerce, outils BI) font 10-20 appels/min. 30/min avec burst=10 gère les pics sans permettre les attaques par énumération. |
odoo_general | 10 req/sec | 20 | Toutes les autres routes | Un utilisateur normal génère 2-5 req/sec pendant la navigation active. 10/sec avec burst=20 gère les chargements de pages avec de nombreuses sous-requêtes. |
Page d'erreur 429 personnalisée
Quand une limite de débit se déclenche, Nginx renvoie un 429 Too Many Requests. Par défaut, c'est une réponse brute. Vous pouvez servir une page conviviale :
# Inside the server block
error_page 429 /429.html;
location = /429.html {
root /var/www/error-pages;
internal;
}
# For API endpoints, return JSON instead of HTML
location = /jsonrpc {
limit_req zone=odoo_api burst=10 nodelay;
limit_req_status 429;
error_page 429 = @rate_limited_json;
proxy_pass http://odoo_backend;
}
location @rate_limited_json {
default_type application/json;
return 429 '{"error": "Rate limit exceeded. Please retry after 60 seconds."}';
}Intégration Fail2Ban
Le rate limiting Nginx renvoie des réponses 429 mais garde la connexion ouverte. Les attaquants persistants consomment toujours des sockets TCP. Pour une protection complète, combinez Nginx avec Fail2Ban pour bannir les IP au niveau du pare-feu :
[odoo-login]
enabled = true
port = http,https
filter = odoo-login
logpath = /var/log/nginx/access.log
maxretry = 10
findtime = 300
bantime = 3600
action = iptables-multiport[name=odoo-login, port="http,https"][Definition]
# Match 429 responses on login endpoints
failregex = ^<HOST>.*"(POST|GET) /web/login.* 429
^<HOST>.*"POST /web/session/authenticate.* 429
^<HOST>.*"(POST|GET) /web/login.* 200.*login_error
ignoreregex =Couche 1 (Nginx) : Ralentit les attaquants à 5 tentatives/minute — transforme une attaque par force brute de 10 secondes en une attaque de 20 minutes. Couche 2 (Fail2Ban) : Après 10 tentatives limitées, bannit l'IP au niveau du pare-feu pendant 1 heure — l'attaquant ne peut même plus établir de connexion TCP. Ensemble, une attaque par force brute qui testerait 10 000 mots de passe en 10 secondes prend désormais plus longtemps que l'entropie du mot de passe pour le craquer.
Optimisation des performances Nginx : Gzip, Cache et Keepalive pour Odoo 19
Sans Nginx, chaque requête de fichier statique (CSS, JS, images) passe par le handler WSGI de Python. C'est environ 10 fois plus lent que Nginx servant le même fichier depuis le système de fichiers ou le cache proxy. Voici la configuration de performance que nous ajoutons par-dessus la config de base :
# ── Gzip Compression ──
gzip on;
gzip_types
text/plain text/css text/javascript
application/json application/javascript
application/xml image/svg+xml;
gzip_min_length 1000;
gzip_comp_level 5;
gzip_vary on;
gzip_proxied any;
# ── Proxy Cache ──
proxy_cache_path /var/cache/nginx/odoo
levels=1:2
keys_zone=odoo_cache:10m
max_size=2g
inactive=60m
use_temp_path=off;
# ── File Handle Cache ──
open_file_cache max=10000 inactive=30s;
open_file_cache_valid 60s;
open_file_cache_min_uses 2;# Hashed assets (Odoo adds unique hashes to URLs)
location ~* /web/static/.*\.(js|css|woff2?|ttf|eot)$ {
proxy_pass http://odoo_backend;
proxy_cache odoo_cache;
proxy_cache_valid 200 365d;
expires 365d;
add_header Cache-Control "public, immutable";
add_header X-Cache-Status $upstream_cache_status;
access_log off;
}
# Dynamic images (product photos, user avatars)
location ~* /web/image/ {
proxy_pass http://odoo_backend;
proxy_cache odoo_cache;
proxy_cache_valid 200 7d;
expires 7d;
add_header Cache-Control "public";
access_log off;
}
# Report PDFs — no caching (dynamic, user-specific)
location ~* /report/ {
proxy_pass http://odoo_backend;
proxy_read_timeout 300s; # Reports can be slow
proxy_buffering off; # Stream to client immediately
}L'impact de ces optimisations en chiffres :
| Métrique | Sans cache Nginx | Avec cache Nginx | Amélioration |
|---|---|---|---|
| JS/CSS statiques | 120ms (via Python) | 2ms (depuis le cache) | 60x plus rapide |
| Images produits | 80ms (requête ORM + redimensionnement) | 5ms (depuis le cache) | 16x plus rapide |
| Chargement page (50 assets) | 3,2s | 0,8s | 4x plus rapide |
| Bande passante (gzip) | 2,4 Mo par chargement | 680 Ko par chargement | 72% de réduction |
3 erreurs Nginx + Odoo qui causent des pannes silencieuses en production
proxy_mode = True manquant dans odoo.conf
Nginx transmet les requêtes avec les en-têtes X-Forwarded-For et X-Forwarded-Proto. Mais si Odoo n'est pas configuré avec proxy_mode = True, il ignore complètement ces en-têtes. Résultat : Odoo pense que chaque requête vient de 127.0.0.1 en HTTP. Les cookies de session n'ont pas le flag Secure, les tokens CSRF échouent de manière intermittente, et toute la journalisation par IP affiche l'adresse loopback de Nginx — rendant vos logs d'accès inutiles pour l'audit de sécurité.
Toujours définir proxy_mode = True dans odoo.conf quand Odoo tourne derrière Nginx. Vérifiez que cela fonctionne en inspectant l'en-tête Set-Cookie dans les DevTools du navigateur — le cookie de session doit inclure Secure; HttpOnly; SameSite=Lax.
Mauvaise configuration WebSocket qui casse Discuss et les notifications
Odoo 19 utilise les WebSockets pour le module Discuss (chat), les notifications en temps réel et le bus polling. L'endpoint WebSocket est /websocket (Odoo 17+), et non /longpolling/poll (Odoo < 17). Si votre config Nginx utilise l'ancien endpoint, les messages de chat n'apparaîtront pas en temps réel — ils ne s'afficheront que quand l'utilisateur rafraîchit la page. La plupart des utilisateurs ne signaleront pas cela comme un « bug » ; ils penseront simplement que le chat Odoo est lent.
Proxifier à la fois /websocket (Odoo 17+) et /longpolling (rétrocompatibilité) vers le port 8072. Inclure les en-têtes Upgrade et Connection pour le handshake WebSocket. Définir proxy_read_timeout à au moins 3600s — la valeur par défaut de 60s fermera les connexions WebSocket inactives.
proxy_buffering casse le téléchargement des gros rapports
Quand Odoo génère un rapport PDF de 50 pages, cela prend 10 à 30 secondes. La stratégie de buffering par défaut de Nginx attend la réponse complète avant de la transmettre au client. Pour les gros rapports, le buffer se remplit, Nginx écrit sur disque (ajoutant de la latence I/O), et l'utilisateur voit un navigateur qui tourne sans indicateur de progression pendant 30+ secondes. Si proxy_read_timeout est trop bas, la connexion se coupe et l'utilisateur obtient une page blanche.
Désactiver le buffering pour le chemin /report/ : proxy_buffering off;. Cela transmet la réponse octet par octet au client. Définir aussi proxy_read_timeout 300s spécifiquement pour les routes de rapports — certains rapports complexes de 10 000+ lignes ont réellement besoin de 5 minutes.
Ce qu'un reverse proxy correctement configuré fait économiser à votre entreprise
Nginx est un logiciel gratuit. Le ROI vient de ce qu'il empêche :
Bloquer /web/database et limiter le débit du login supprime les deux vecteurs d'attaque les plus courants contre les installations Odoo.
Le cache statique + gzip réduit le temps de chargement moyen de 3,2s à 0,8s. Les utilisateurs passent moins de temps à attendre, plus de temps à travailler.
La compression gzip réduit la taille des transferts de 72%. C'est important pour les équipes distantes sur des connexions lentes et pour le coût d'hébergement.
Au-delà des chiffres : les auditeurs SOC 2, ISO 27001 et PCI-DSS s'attendent à une couche de reverse proxy. Exposer Odoo directement sur internet sans terminaison SSL, rate limiting et durcissement des endpoints est une non-conformité qui peut retarder la certification de plusieurs mois.
Métadonnées d'optimisation
Configuration complète du reverse proxy Nginx pour Odoo 19. Bloquer /web/database, limiter le débit des endpoints login et API, activer SSL, gzip et le cache statique.
1. « Configuration Nginx complète pour Odoo 19 avec SSL et support WebSocket »
2. « Rate Limiting Nginx pour Odoo 19 : protéger les endpoints Login, API et RPC »
3. « 3 erreurs Nginx + Odoo qui causent des pannes silencieuses en production »