Se rendre au contenu

Sécuriser Odoo 19 en production : Nginx, SSL Let's Encrypt et VPS Debian

Bloc 6 · Infrastructure — Article 1/2 Sécuriser Odoo 19 en production : Nginx, SSL Let's Encrypt, VPS Debian Passer d'un Odoo de dev en localhost à un Odoo…
26 avril 2026 par
Sécuriser Odoo 19 en production : Nginx, SSL Let's Encrypt et VPS Debian
B.Mustapha

Bloc 6 · Infrastructure — Article 1/2

Sécuriser Odoo 19 en production : Nginx, SSL Let's Encrypt, VPS Debian

Passer d'un Odoo de dev en localhost à un Odoo exposé publiquement implique 3 couches — un reverse proxy Nginx, un certificat TLS Let's Encrypt, et un service systemd robuste. Guide terrain basé sur un VPS Debian 12 réel (mon propre VPS LWS qui héberge OdooSkills).

~13 minutes de lecture · commandes vérifiées sur Debian 12 + Odoo 19 CE

L'architecture cible

3 briques à configurer dans l'ordre — Odoo liaison locale, Nginx reverse proxy, SSL via certbot. Chacune ajoute une couche de sécurité sans impacter les autres.

Architecture Nginx SSL Let's Encrypt Odoo 19 VPS Debian

1 — Prérequis : VPS Debian 12 + DNS prêt

Avant toute chose, tu as besoin de :

  • Un VPS Debian 12 (2 vCPU, 4 Go RAM minimum pour 20 utilisateurs)
  • Un nom de domaine (ex. erp.monentreprise.dz) avec un enregistrement A pointant l'IP publique du VPS
  • Un Odoo 19 CE déjà installé et tournant sur 127.0.0.1:8069 (cf. article T01 de la série)
  • Un accès root SSH par clé publique

Vérification rapide DNS :

# Depuis ton poste local
dig +short erp.monentreprise.dz
# → doit retourner l'IP publique de ton VPS
# Ex: 195.110.35.177
Pas de DNS, pas de Let's Encrypt. Certbot valide le domaine via une requête HTTP sur /.well-known/acme-challenge/ — si le domaine ne pointe pas vers ton VPS, l'émission du cert échoue.

2 — Verrouiller Odoo sur l'interface loopback

En prod, Odoo ne doit jamais écouter sur 0.0.0.0 (toutes les interfaces). Le reverse proxy Nginx est le seul interlocuteur externe — Odoo reste derrière. Éditer /etc/odoo/odoo.conf :

[options]
; Bind loopback seulement
xmlrpc_interface = 127.0.0.1
longpolling_port = 8072

; Proxy mode : Odoo fait confiance au X-Forwarded-*
proxy_mode = True

; Filestore persistant
data_dir = /var/lib/odoo/filestore

; Log
logfile = /var/log/odoo/odoo.log
log_level = info
log_handler = :INFO

; DB
db_host = localhost
db_port = 5432
db_user = odoo
db_password = CHANGE_ME

; Admin
admin_passwd = CHANGE_ME_TOO_LONG
list_db = False

; Workers pour prod (nombre de vCPU × 2 + 1)
workers = 5
max_cron_threads = 2
limit_memory_hard = 2684354560
limit_memory_soft = 2147483648
limit_time_cpu = 600
limet_time_real = 1200

Le flag proxy_mode = True est critique : sans lui, Odoo ignore les headers X-Forwarded-Proto et X-Real-IP que Nginx transmet, et génère des redirections HTTP incorrectes.

Redémarrer et vérifier :

systemctl restart odoo
ss -tlnp | grep -E '8069|8072'
# → doit afficher 127.0.0.1:8069 et 127.0.0.1:8072 uniquement
# Jamais 0.0.0.0:*

3 — Installer et configurer Nginx

apt update
apt install -y nginx
systemctl enable --now nginx

Créer le vhost pour Odoo. Fichier /etc/nginx/sites-available/odoo :

upstream odoo {
    server 127.0.0.1:8069;
}
upstream odoo_chat {
    server 127.0.0.1:8072;
}

# Bloc HTTP — redirection vers HTTPS
server {
    listen 80;
    listen [::]:80;
    server_name erp.monentreprise.dz;

    # ACME challenge reste en clair (exigence certbot)
    location /.well-known/acme-challenge/ {
        root /var/www/html;
    }

    # Tout le reste en HTTPS
    location / {
        return 301 https://$host$request_uri;
    }
}

# Bloc HTTPS — proxy vers Odoo
server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name erp.monentreprise.dz;

    # Certificats Let's Encrypt (ajoutés par certbot à l'étape 4)
    # ssl_certificate     /etc/letsencrypt/live/erp.monentreprise.dz/fullchain.pem;
    # ssl_certificate_key /etc/letsencrypt/live/erp.monentreprise.dz/privkey.pem;

    # Sécurité TLS moderne (Mozilla Intermediate 2024)
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers off;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 1d;
    ssl_session_tickets off;

    # Headers de sécurité
    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
    add_header X-Content-Type-Options nosniff;
    add_header X-Frame-Options SAMEORIGIN;
    add_header Referrer-Policy strict-origin-when-cross-origin;

    # Logs dédiés
    access_log /var/log/nginx/odoo.access.log;
    error_log  /var/log/nginx/odoo.error.log;

    # Limites upload (imports CSV lourds, photos produits)
    client_max_body_size 100M;

    # Proxy général vers Odoo
    location / {
        proxy_pass http://odoo;
        proxy_read_timeout 720s;
        proxy_connect_timeout 720s;
        proxy_send_timeout 720s;
        proxy_http_version 1.1;

        # Headers pour proxy_mode=True côté Odoo
        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;

        proxy_redirect off;
    }

    # Canal long-polling (notifications chatter temps réel)
    location /longpolling {
        proxy_pass http://odoo_chat;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_read_timeout 720s;
    }

    # Cache statique (assets compilés, images)
    location ~* /web/static/ {
        proxy_cache_valid 200 60m;
        proxy_buffering on;
        expires 864000;
        proxy_pass http://odoo;
    }

    # Gzip
    gzip on;
    gzip_min_length 1100;
    gzip_buffers 4 32k;
    gzip_types text/css text/less text/plain text/xml application/xml application/json application/javascript;
    gzip_vary on;
}

Activer le vhost et tester :

ln -s /etc/nginx/sites-available/odoo /etc/nginx/sites-enabled/
rm -f /etc/nginx/sites-enabled/default
nginx -t   # vérification syntaxe
systemctl reload nginx
Note : à ce stade le bloc HTTPS référence des certificats qui n'existent pas encore — j'ai mis les lignes ssl_certificate* en commentaire pour que nginx -t passe. Décommente après l'étape 4.

4 — Obtenir un certificat Let's Encrypt avec certbot

Installer certbot + plugin Nginx :

apt install -y certbot python3-certbot-nginx

Lancer l'obtention du certificat :

certbot --nginx -d erp.monentreprise.dz \
    --email admin@monentreprise.dz \
    --agree-tos --no-eff-email --redirect

Certbot fait 3 choses automatiquement :

  1. Génère une paire clé/cert via challenge HTTP-01 sur /.well-known/acme-challenge/
  2. Stocke la clé dans /etc/letsencrypt/live/erp.monentreprise.dz/
  3. Décommente les lignes ssl_certificate* dans le vhost Nginx et recharge le service

Vérifier le résultat :

curl -I https://erp.monentreprise.dz
# → HTTP/2 303 (redirection web → /odoo)
# → strict-transport-security: max-age=63072000; includeSubDomains
# → server: nginx/1.22.1

certbot certificates
# → Certificate Name: erp.monentreprise.dz
# → Domains: erp.monentreprise.dz
# → Expiry Date: 2026-07-14 (90 jours)
Cadenas vert dans le navigateur — notes cryptographiques : TLS 1.3, ECDHE-ECDSA-AES256-GCM-SHA384, chain complète via ISRG Root X1. Score SSL Labs visé : A ou A+.

5 — Auto-renouvellement du certificat

Let's Encrypt émet des certs valides 90 jours. Les laisser expirer = site inaccessible en HTTPS. Certbot installe automatiquement un timer systemd :

systemctl list-timers | grep certbot
# → certbot.timer activé, tick 2×/jour

# Tester le renouvellement à blanc (dry-run)
certbot renew --dry-run
# → Congratulations, all simulated renewals succeeded

Le certbot renouvelle uniquement les certs expirant < 30 jours. Donc 2 fois par an dans la pratique. Zéro action manuelle nécessaire — mais surveille quand même les logs :

journalctl -u certbot.service --since "7 days ago"

Optionnel mais recommandé — recharger Nginx après chaque renouvellement. Créer /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh :

#!/bin/bash
systemctl reload nginx
chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh

6 — Durcissement final (hardening)

Le setup marche, SSL vert, Odoo accessible. 5 derniers gestes qui distinguent une prod sérieuse d'un déploiement dilettante :

6.1 — Firewall UFW

apt install -y ufw
ufw default deny incoming
ufw default allow outgoing
ufw allow 22/tcp   # SSH (change le port en prod)
ufw allow 80/tcp   # HTTP (pour redirection + ACME)
ufw allow 443/tcp  # HTTPS
ufw enable
ufw status numbered

Odoo reste invisible de l'extérieur — 8069 n'est jamais ouvert.

6.2 — Fail2ban sur Nginx

apt install -y fail2ban
systemctl enable --now fail2ban

Configuration /etc/fail2ban/jail.local :

[sshd]
enabled = true
maxretry = 3
bantime = 1h

[nginx-http-auth]
enabled = true
filter = nginx-http-auth
logpath = /var/log/nginx/odoo.error.log

[nginx-noscript]
enabled = true
filter = nginx-noscript
logpath = /var/log/nginx/odoo.access.log
maxretry = 6

6.3 — list_db = False

Critique : dans /etc/odoo/odoo.conf tu as list_db = False. Sans ça, n'importe quel visiteur peut lister tes bases via /web/database/manager — risque de bruteforce sur admin_passwd.

6.4 — Backup automatique

Cron quotidien vers un stockage distant (cf. article T07 de la série) :

# /etc/cron.daily/backup-odoo
#!/bin/bash
set -e
DATE=$(date +%Y%m%d)
DEST=/var/backups/odoo

mkdir -p $DEST
pg_dump -U odoo -Fc mydb > $DEST/db-$DATE.dump
tar czf $DEST/filestore-$DATE.tgz -C /var/lib/odoo/filestore mydb

# Rétention 14 jours
find $DEST -name 'db-*.dump' -mtime +14 -delete
find $DEST -name 'filestore-*.tgz' -mtime +14 -delete

# Sync offsite (rclone → S3/Wasabi/Backblaze)
rclone copy $DEST remote:odoo-backups/$(hostname)/

6.5 — Monitoring

Au minimum : uptime externe (UptimeRobot gratuit) + alerte email si HTTP 5xx. Optionnel : Netdata / Prometheus pour métriques système.

Checklist de mise en production

  • ✅ DNS A record pointe vers l'IP VPS
  • ✅ Odoo binding loopback uniquement (xmlrpc_interface = 127.0.0.1)
  • proxy_mode = True dans odoo.conf
  • admin_passwd fort (> 24 caractères)
  • list_db = False
  • ✅ Nginx vhost + HTTPS redirect
  • ✅ Certificat Let's Encrypt émis
  • certbot renew --dry-run OK
  • ✅ UFW actif, 22/80/443 uniquement
  • ✅ Fail2ban actif sur SSH + Nginx
  • ✅ Backup quotidien vers stockage offsite
  • ✅ Uptime monitoring externe

En résumé

Une mise en prod Odoo 19 sérieuse tient en 6 étapes et environ 2 heures la première fois (30 minutes les suivantes). Les 3 erreurs que je vois trop souvent :

  • Odoo bind 0.0.0.0 et firewall ouvert → port 8069 accessible publiquement
  • proxy_mode=False + reverse proxy HTTPS → redirections incohérentes, cookies cassés
  • Pas de auto-renew testé → cert expire un dimanche, site HS 18h

Évite ces 3 pièges et ta stack Odoo 19 tient la route plusieurs années sans friction. C'est exactement le setup qui fait tourner https://odooskills.com aujourd'hui.

Prochain article (T25) : passer à la couche applicative Website — créer ton site vitrine Odoo sans écrire une ligne de HTML, maîtriser les menus, les thèmes, le SEO on-page et les extensions utiles.

Controllers HTTP et API REST en Odoo 19 : http.Controller, @http.route et modes d'authentification
Bloc 5 · Article 4/4 — Clôture du Parcours Fondamentaux Controllers HTTP et API REST en Odoo 19 Exposer ton module au monde extérieur — http.Controller , @http.