Guide13 mars 2026

Reverse Proxy Nginx pour Odoo 19 :
Sécurité, Rate Limiting et Performance

INTRODUCTION

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.

01

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.

Nginx — /etc/nginx/sites-available/odoo
# ── 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;
    }
}
02

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 :

EndpointCe qu'il exposeVecteur d'attaqueRègle Nginx
/web/database/*Gestion complète de la BDD : créer, supprimer, sauvegarder, restaurer, dupliquerForce brute du mot de passe maître → suppression de la BDD de productiondeny all; return 404;
/web/webclient/version_infoVersion exacte d'Odoo, infos serveur, modules installésFingerprinting → cibler les CVE connues pour cette version exactedeny all; return 404;
/web/debugActivation du mode debug, débogage des assets, profileurDivulgation d'informations, dégradation des performancesdeny all; return 404;
/web/loginEndpoint d'authentificationCredential stuffing, force brutelimit_req 5r/m
/jsonrpc / /xmlrpcSurface API complète (CRUD sur tous les modèles)Abus d'API, exfiltration de données via identifiants voléslimit_req 30r/m
Pourquoi renvoyer 404 et non 403 ?

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 :

Nginx — Gestionnaire de base de données avec whitelist
# 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;
}
03

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

ZoneDébitBurstEndpoints protégésPourquoi ce débit
odoo_login5 req/min3/web/login, /web/session/authenticate, /web/reset_passwordUn 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_api30 req/min10/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_general10 req/sec20Toutes les autres routesUn 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 :

Nginx — Réponse personnalisée de rate limit
# 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 :

INI — /etc/fail2ban/jail.d/odoo-nginx.conf
[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"]
INI — /etc/fail2ban/filter.d/odoo-login.conf
[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 =
Défense à deux couches

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.

04

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 :

Nginx — Directives de performance (bloc http)
# ── 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;
Nginx — Cache des assets statiques (bloc server)
# 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étriqueSans cache NginxAvec cache NginxAmélioration
JS/CSS statiques120ms (via Python)2ms (depuis le cache)60x plus rapide
Images produits80ms (requête ORM + redimensionnement)5ms (depuis le cache)16x plus rapide
Chargement page (50 assets)3,2s0,8s4x plus rapide
Bande passante (gzip)2,4 Mo par chargement680 Ko par chargement72% de réduction
05

3 erreurs Nginx + Odoo qui causent des pannes silencieuses en production

1

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é.

Notre correctif

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.

2

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.

Notre correctif

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.

3

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.

Notre correctif

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.

ROI BUSINESS

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 :

0 €Coût de violation

Bloquer /web/database et limiter le débit du login supprime les deux vecteurs d'attaque les plus courants contre les installations Odoo.

4xChargement plus rapide

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.

72%Moins de bande passante

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.

NOTES SEO

Métadonnées d'optimisation

Meta Desc

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.

Mots-clés H2

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 »

N'exposez pas Odoo à nu sur internet

Chaque instance Odoo accessible depuis internet a besoin d'un reverse proxy. Pas comme un « bonus » — comme une exigence de sécurité de base. Sans cela, votre gestionnaire de base de données est à une URL de l'accès public, votre page de connexion est vulnérable à la force brute à la vitesse machine, et vos fichiers statiques sont servis par Python au lieu d'un serveur web conçu pour cela.

Si vous n'êtes pas sûr que votre installation Odoo est correctement sécurisée derrière Nginx, nous pouvons l'auditer. Nous vérifions l'exposition des endpoints, la configuration SSL, le rate limiting, l'efficacité du cache et la connectivité WebSocket. L'audit prend quelques heures et produit un rapport de durcissement actionnable avec les modifications exactes de la configuration Nginx.

Réserver un audit sécurité gratuit