Faire fonctionner Odoo derrière nginx ou Cloudflare est un travail de dix minutes. Le faire fonctionner correctement est une autre histoire, et l'écart entre les deux est inhabituellement silencieux : Odoo servira des pages, les utilisateurs se connecteront, rien n'apparaîtra dans les journaux, et quelques éléments seront subtilement erronés pendant des mois.
Si vous êtes ici parce que votre site fonctionne mais que les URL sont incorrectes quelque part (emails liant à http://, un sitemap qui ne passe pas en https, hreflang qui ne s'affiche jamais, la mauvaise IP dans chaque ligne de journal), c'est presque toujours la même cause unique.
La version courte
Définir proxy_mode = True dans odoo.conf ne fait rien par lui-même. Odoo ne le respecte que lorsque la requête entrante porte déjà un en-tête X-Forwarded-Host. Manquez cet en-tête et le paramètre est complètement ignoré, y compris X-Forwarded-Proto, qui est celui que la plupart des guides vous disent de définir.
Vous avez besoin que ces trois conditions soient vraies :
- proxy_mode = True dans odoo.conf
- Odoo redémarré, car c'est une option de fichier de configuration, pas un paramètre d'exécution
- Votre proxy envoie à la fois X-Forwarded-Host et X-Forwarded-Proto
Manquez-en un et Odoo construit chaque URL absolue à partir de l'adresse à laquelle il a été atteint en interne (http://localhost:8069) plutôt que de l'adresse que vos utilisateurs ont tapée.
Pourquoi l'en-tête supplémentaire est requis
Ceci est la vérification qu'Odoo effectue sur chaque demande, dans Application.__call__ (odoo/http.py, Odoo 19.0):
if odoo.tools.config['proxy_mode'] and environ.get("HTTP_X_FORWARDED_HOST"):
...
ProxyFix(fake_app)(environ, fake_start_response)
Deux conditions jointes par et. proxy_mode est votre drapeau de configuration ; HTTP_X_FORWARDED_HOST est la clé d'environnement WSGI pour l'en-tête X-Forwarded-Host. Si l'en-tête est absent, le middleware ne s'exécute jamais, et aucun en-tête transféré n'est de confiance.: ni le proto, ni le for, ni l'hôte.
C'est délibéré, et c'est le bon design. Les en-têtes transférés sont des chaînes fournies par le client : quiconque peut atteindre Odoo directement peut prétendre être n'importe quel hôte, depuis n'importe quelle IP, sur n'importe quel schéma. L'aide de configuration d'Odoo le dit clairement :
--proxy-mode ... “Activez les wrappers WSGI de proxy inverse (réécriture des en-têtes). N'activez cela que lorsque vous êtes derrière un proxy web de confiance !” Source : odoo/tools/config.py
La présence de X-Forwarded-Host est utilisée comme signal qu'un proxy est réellement devant. Ce n'est pas une frontière de sécurité à elle seule (cela doit venir de votre pare-feu), mais cela signifie que le drapeau ne peut pas être laissé activé dans un fichier de configuration et commencer silencieusement à faire confiance aux en-têtes de l'internet ouvert.
La conséquence pratique est ce qui compte : un proxy partiellement configuré se comporte exactement comme un non configuré.. Il n'y a pas d'état semi-fonctionnel et pas d'avertissement.
Ce à quoi Odoo fait réellement confiance.
Lorsque la porte s'ouvre, Odoo applique le ProxyFix de Werkzeug avec des paramètres fixes :
ProxyFix = functools.partial(ProxyFix_, x_for=1, x_proto=1, x_host=1)
Cette seule ligne détermine tout ce que votre configuration de proxy doit satisfaire :
En-tête | Réécritures | Fiable ? |
|---|---|---|
X-Forwarded-For | REMOTE_ADDR, l'IP du client sur laquelle Odoo journalise et limite le taux | 1 saut |
X-Forwarded-Proto | wsgi.url_scheme, http vs https dans chaque URL générée | 1 saut |
X-Forwarded-Host | HTTP_HOST, SERVER_NAME, SERVER_PORT | 1 saut |
X-Forwarded-Port | n/a | ignoré |
X-Forwarded-Prefix | n/a | ignoré |
Deux d'entre eux méritent de l'attention.
X-Forwarded-Port n'est pas lu. Odoo prend le port à partir du composant port de X-Forwarded-Host à la place. Si vous servez sur un port non standard, il doit être dans cet en-tête (X-Forwarded-Host: erp.example.com:8443), pas dans X-Forwarded-Port, que Odoo ignore.
X-Forwarded-Prefix n'est pas lu non plus, ce qui signifie que SCRIPT_NAME n'est jamais réécrit. Servir Odoo à partir d'un sous-chemin d'un site plus grand (example.com/erp/) n'est pas pris en charge par ce mécanisme, peu importe à quel point votre proxy réécrit le chemin. Odoo continuera à générer des URL relatives à la racine. Donnez à Odoo son propre nom d'hôte ou sous-domaine.
Le 1 est la partie qui fait mal : vraies IPs des clients
x_for=1 ne signifie pas "prendre la première entrée." Werkzeug le résout comme values[-trusted]: la valeur la plus à droite dans l'en-tête, en comptant un position en arrière.
Avec un seul proxy, c'est correct. Avec deux, ce n'est pas silencieusement le cas.
Une pile très courante est Cloudflare devant nginx. Cloudflare arrive avec l'IP du visiteur dans X-Forwarded-For. nginx, configuré comme presque tous les tutoriels le décrivent, ajoute ensuite sa propre vue de la connexion :
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # ajoute : deux sauts maintenant
Odoo reçoit X-Forwarded-For : 203.0.113.9, 172.68.x.x, prend la valeur la plus à droite, et enregistre l'IP de bord de Cloudflare comme client. Chaque ligne de journal, chaque enregistrement de connexion, chaque limite de taux et chaque piste d'audit pointe maintenant vers votre CDN au lieu d'un utilisateur. Rien ne casse ; les données sont simplement incorrectes, et elles restent incorrectes rétroactivement.
Derrière Cloudflare, donnez à Odoo le visiteur directement :
# Cloudflare met la véritable IP du client dans son propre en-tête
proxy_set_header X-Forwarded-For $http_cf_connecting_ip;
La version plus robuste est le real_ip module de nginx avec les plages IP publiées par Cloudflare dans set_real_ip_from, real_ip_header CF-Connecting-IP, et ensuite proxy_set_header X-Forwarded-For $remote_addr;. De cette façon $remote_addr est déjà le visiteur partout dans votre configuration nginx, y compris vos propres journaux d'accès. Cloudflare publie ces plages et elles changent ; tirez-les plutôt que de les coller.
La règle générale : Odoo fait confiance à exactement un saut, alors assurez-vous que le dernier saut dit la vérité.
Ce qu'un proxy mal configuré casse réellement
Aucun de ces éléments ne s'annonce. Voici la liste à vérifier, car ces symptômes sont généralement signalés comme quatre problèmes non liés.
Les URL absolues sont construites à partir du mauvais hôte. Tout ce qu'Odoo génère en dehors de la barre d'adresse du navigateur (liens de réinitialisation de mot de passe, invitations au portail, liens de partage, og:url, liens de rapport envoyés par e-mail) est assemblé à partir de HTTP_HOST et du schéma d'URL. Mauvais hôte, mauvais schéma, mauvais lien. Les utilisateurs remarquent cela en premier, et généralement comme “le lien de réinitialisation est cassé.”
robots.txt et sitemap.xml émettent http://. Le contrôleur de sitemap construit <loc> valeurs à partir de url_root. Si wsgi.url_scheme est http, chaque URL que vous soumettez aux moteurs de recherche est le mauvais schéma.
Le bloc hreflang du site web disparaît complètement. Sur un site multilingue, celui-ci est réellement invisible sans afficher la source. Odoo contrôle les balises hreflang sur website._is_canonical_url(), qui compare :
current_url = request.httprequest.url_root[:-1] + request.httprequest.environ['REQUEST_URI']
canonical_url = self.env['ir.http']._url_localized(..., canonical_domain=self.get_base_url())
return current_url == canonical_url
url_root provient de l'environnement WSGI, donc c'est http:// derrière un proxy non corrigé. get_base_url() renvoie votre domaine configuré, qui est https://. Ils ne peuvent jamais être égaux, la vérification renvoie False sur chaque page, et le bloc hreflang est ignoré sur l'ensemble du site, sur un site dont les traductions sont par ailleurs parfaitement configurées.
Notez que celui-ci n'est pas réparable depuis la base de données. Définir web.base.url ne aide pas, car rien dans les paramètres système ne peut changer url_root. Il vient de l'environnement WSGI, qui est exactement ce que ProxyFix existe pour corriger. Du temps est régulièrement perdu ici, car web.base.url est le paramètre qui semble être responsable. (Le diagnostic complet est un post à part ; liez le guide hreflang ici lors de la publication.)
Le filtrage d'hôte multi-base de données sélectionne la mauvaise base de données, ou aucune. db_filter() résout %h et %d à partir de l'environnement :
host = request.httprequest.environ.get('HTTP_HOST', '')HTTP_HOST est l'une des valeurs ProxyFix réécrit. Sans cela, chaque requête semble arriver à n'importe quelle adresse interne que votre proxy a utilisée, donc un dbfilter basé sur le nom d'hôte correspond à la même base de données pour chaque domaine, ou ne correspond à rien, et présente un sélecteur de base de données que vous pensiez avoir désactivé. Sur un hôte multi-locataire, c'est la différence entre le routage par domaine et le non-routage.
Un détail de mise en cache à connaître. Le plan du site est stocké comme un ir.attachment basé sur un hachage de url_root. Corriger le schéma génère donc un nouveau plan du site mis en cache immédiatement plutôt que de vous faire attendre la durée de vie, mais le http:// pièce jointe reste dans la base de données. Supprimez-le, ou vous continuerez à le trouver.
Cloudflare : utilisez Full (strict), pas Flexible
Cloudflare’s Flexible Le mode SSL crypte le navigateur→Cloudflare et communique ensuite avec votre origine via HTTP en clair. Il est présenté comme l'option zéro-configuration, et il interagit mal avec une origine correctement sécurisée.
L'échec est une boucle de redirection. Votre origine, de manière sensée, redirige HTTP vers HTTPS. Cloudflare, en mode Flexible, ne se connecte jamais qu'en HTTP. Donc : Cloudflare demande via HTTP → l'origine renvoie 301 → https:// → Cloudflare demande à nouveau via HTTP → boucle, jusqu'à ce que le navigateur abandonne avec ERR_TOO_MANY_REDIRECTS. La réaction habituelle est de supprimer la redirection HTTPS de l'origine, ce qui résout la boucle en rendant l'origine définitivement non chiffrée.
Le deuxième problème est celui que vous ne pouvez pas voir : en mode Flexible, le trafic entre Cloudflare et votre serveur traverse l'internet public en texte clair, cookies de session inclus, tandis que le navigateur affiche un cadenas.
Utilisez Complet (strict) avec un certificat valide sur l'origine. Let’s Encrypt est gratuit, ou les certificats CA d'origine de Cloudflare sont délivrés pour 15 ans et sont spécifiquement approuvés par Cloudflare à cette fin.
Websockets et le deuxième port
Odoo exécute son bus sur un deuxième processus, sur --gevent-port, 8072 par défaut (odoo/tools/config.py). Les requêtes vers /websocket doivent atteindre ce port, pas 8069, et doivent être autorisées à mettre à niveau la connexion.
Si vous vous trompez, le symptôme est étrangement spécifique : l'application fonctionne, mais les fonctionnalités en direct s'arrêtent silencieusement : les messages de chat et de discussion n'arrivent pas, les notifications n'apparaissent pas, et la console du navigateur se remplit de connexions websocket échouées. Les opérations de longue durée qui rapportent des progrès semblent se bloquer.
Une configuration nginx fonctionnelle
# Nécessaire pour les mises à niveau websocket
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
upstream odoo { server 127.0.0.1:8069; }
upstream odoo_bus { server 127.0.0.1:8072; }
server {
écouter 443 ssl;
http2 activé;
server_name erp.example.com;
ssl_certificate /etc/letsencrypt/live/erp.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/erp.example.com/privkey.pem;
# Les quatre en-têtes. X-Forwarded-Host est celui qui ouvre la porte.
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # voir la note Cloudflare ci-dessus
proxy_set_header X-Real-IP $remote_addr;
# Importations longues, grands rapports, génération de PDF
proxy_read_timeout 720s;
proxy_connect_timeout 720s;
proxy_send_timeout 720s;
client_max_body_size 100m;
location / {
proxy_pass http://odoo;
proxy_redirect off;
}
location /websocket {
proxy_pass http://odoo_bus;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
# Mise en cache longue des bundles d'actifs hachés d'Odoo
location ~* /web/(static|assets)/ {
proxy_pass http://odoo;
proxy_cache_valid 200 302 60m;
expires 864000;
}
}
server {
listen 80;
server_name erp.example.com;
return 301 https://$host$request_uri;
}
Et dans odoo.conf:
proxy_mode = True
Puis redémarrez Odoo. Il est lu uniquement au démarrage.
Fermez la route directe. proxy_mode indique à Odoo de faire confiance aux en-têtes transférés ; seul votre pare-feu empêche quelqu'un de les envoyer directement. Liez Odoo à la boucle locale (http_interface = 127.0.0.1) ou bloquez 8069 et 8072 au niveau du pare-feu. Sinon, quiconque peut atteindre l'origine peut usurper à la fois son IP et son nom d'hôte.
Vérification, en trois commandes
Ne vérifiez pas cela en chargeant le site dans un navigateur : le navigateur cache précisément ce qui ne va pas. Vérifiez les URL générées par Odoo.
1. Le test en une ligne. Le plan du site est construit à partir de url_root, donc il rapporte le schéma qu'Odoo pense servir :
curl -s -L https://erp.example.com/sitemap.xml | grep -o '<loc>[^<]*</loc>' | head -3
Toujours http:// ? url_root est toujours incorrect, et rien d'autre ne vaut la peine d'être vérifié tant que cela n'est pas corrigé.
2. robots.txt, qui contient une directive de plan de site absolue :
curl -s https://erp.example.com/robots.txt | grep -i sitemap
3. Sur un site multilingue, confirmer les retours hreflang, en utilisant curl plutôt que devtools, donc vous lisez ce qu'un crawler reçoit plutôt qu'un DOM rendu :
curl -s -A "Googlebot" https://erp.example.com/ | grep -o '<link[^>]*hreflang[^>]*>'
Une sortie vide sur un site avec plus d'une langue active signifie que la comparaison canonique échoue toujours.
Rerun the first check avec une chaîne de requête (?utm_source=test). La faute originale dépend du schéma, et un correctif partiel qui fonctionne sur des URLs propres est possible.
La partie qui n’est pas dans le fichier de configuration
Tout ce qui précède est un travail unique qui reste correct jusqu'à ce que quelque chose change : un nouveau certificat, un CDN en avant, une deuxième base de données, une langue ajoutée, une migration qui réinitialise un fichier de configuration. Chacune de ces choses peut silencieusement rouvrir le même écart, et aucune ne se manifestera comme une erreur. Le mode de défaillance de la couche proxy est qu'il continue de fonctionner tout en obtenant discrètement les détails incorrects, c'est pourquoi ces problèmes sont généralement découverts des mois plus tard par quelqu'un qui audite autre chose.
C’est la véritable différence entre Odoo en cours d'exécution et Odoo en cours d'exécution correctement. C'est rarement dramatique. C'est un en-tête que personne n'a défini, et il reste ainsi jusqu'à ce que quelqu'un lise la source.
Exécutez-le sur votre propre serveur, sans posséder ce problème
Si vous préférez ne pas maintenir la couche proxy, le renouvellement des certificats, le routage websocket et l'en-tête vous-même : Skysize BYOS fonctionne sur votre serveur, dans votre pays, sous votre contrôle, et nous gérons le côté Odoo, y compris tout ce qui précède. Construit par des ex-ingénieurs Odoo.
Connectez votre serveur. Ou, si vous préférez ne pas exécuter la machine non plus, voir hébergement Odoo géré.