← Back to guides

Odoo security

How to secure Odoo on a VPS before going live

This guide is an audit-first security and hardening checklist for self-hosted Odoo environments. It is useful before exposing a new Odoo instance, and also for reviewing an existing Odoo VPS that is already online.

New Odoo install

Use this checklist before connecting a domain, opening the instance to users, or logging in over the public internet.

Existing Odoo server

Use this checklist to audit an Odoo VPS that is already online and verify that dangerous ports, routes, and settings are not exposed.

Target secure architecture

A safer self-hosted Odoo setup does not expose Odoo or PostgreSQL directly to the internet. Public traffic should reach Nginx first, then Nginx should forward trusted requests to local Odoo services.

Internet
  → HTTPS / Nginx
  → 127.0.0.1:8069 Odoo
  → private PostgreSQL

Public entry point

Nginx on 80/443

Only the reverse proxy should receive public web traffic. HTTPS should be enforced before real users log in.

Odoo application

127.0.0.1:8069

Odoo should not be exposed directly to the internet. Nginx should proxy traffic to Odoo locally.

Odoo websocket

127.0.0.1:8072

If websocket or longpolling is enabled, it should also stay local and be proxied through Nginx.

PostgreSQL

Private only

PostgreSQL should be reachable only by Odoo or trusted internal services, never publicly exposed.

Read this before running commands

This checklist is not a full forensic audit

This guide helps you perform a first-level server audit and detect common exposure or maintenance problems: public ports, weak SSH settings, exposed PostgreSQL, risky Odoo routes, suspicious cron jobs, unknown SSH keys, unusual login activity, and abnormal resource usage.

Finding an anomaly does not always prove compromise, but it should trigger deeper investigation. Examples include unknown successful SSH logins, unfamiliar users with shell access, hidden scripts, cron jobs downloading remote code, unknown entries in authorized_keys, or processes pretending to be normal system tools while consuming high CPU or opening suspicious network connections.

If you find signs of compromise, do not simply delete one suspicious file and continue. Contain the issue, preserve evidence if needed, rotate secrets, review persistence mechanisms, and consider rebuilding from a clean snapshot. For production systems, client data, or regulated business data, this usually deserves a deeper review by someone qualified to audit and secure Linux servers.

This page is designed as an , not a . Some checks are read-only, but others describe changes that can affect , , running Odoo services, or production availability.
Before changing firewall rules, SSH settings, Docker ports, Nginx, or Odoo configuration, , confirm you have , and make sure you have a .

Safe check

Read-only commands or manual checks that should not change the server state.

Caution

Checks that may reveal secrets or require careful interpretation before taking action.

Change required

Actions that can affect access, running services, or production availability. Plan before applying.

Reference: important odoo.conf settings

The Odoo configuration file is one of the most important parts of a secure deployment. This reference explains the settings you should understand before going live. The audit checklist below then shows when and how to locate and review the active odoo.conf file. Do not paste the full configuration publicly because it may contain secrets.

Show commands to find and read odoo.conf
# Find possible Odoo configuration files
sudo find / -name "odoo.conf" 2>/dev/null

# Read a common Linux package installation config
sudo less /etc/odoo/odoo.conf

# For Docker Compose installs, the config may be mounted from the project folder
less ./config/odoo.conf

# List running containers to identify the Odoo container name
docker ps --format "table {{.Names}}\t{{.Image}}\t{{.Ports}}"

# Check whether a running Odoo container has /etc/odoo mounted
docker inspect YOUR_ODOO_CONTAINER_NAME --format '{{range .Mounts}}{{println .Source "->" .Destination}}{{end}}'

# Safer review: show only important security-related settings
sudo grep -E "^(admin_passwd|db_host|db_port|db_user|db_password|proxy_mode|list_db|dbfilter|log_level|workers|max_cron_threads|limit_time_cpu|limit_time_real)" /path/to/odoo.conf | sed -E 's/(admin_passwd|db_password) = .+/\1 = ***hidden***/'

Use less to read the file safely without editing it. Press q to quit. If your Odoo runs in Docker, inspect the mounted config path from your docker-compose.yml file instead of assuming the config is in /etc/odoo/odoo.conf.

If no odoo.conf file is found, Odoo may not be installed on the server, may be running from a container without a mounted config file, or may be managed from a different directory. In that case, first identify how Odoo is installed before changing anything.

In Docker Compose deployments, odoo.conf is often created manually and mounted into the container. In package-based installations, it may be created under /etc/odoo/odoo.conf. Do not assume the first file you find is active. Check the service, container mounts, or startup command to confirm which configuration file Odoo is really using.

admin_passwd

Use a strong random secret

This protects sensitive database manager actions. Never keep a default, weak, or reused master password.

proxy_mode

True behind a trusted reverse proxy

This lets Odoo correctly understand HTTPS and proxy headers when Nginx is in front of it.

list_db

False for production-style single-database setups

This helps prevent public database listing and reduces exposure of database manager screens.

dbfilter

Restrict to the intended database

This prevents accidental exposure of development, test, or unrelated databases on the same Odoo server.

log_level

info or warn

Debug logs can become noisy and may expose sensitive operational details in production.

workers

Configure for production when needed

Workers improve reliability for real traffic. They also affect websocket and longpolling configuration.

limit_time_cpu / limit_time_real

Set reasonable limits

These limits help prevent long-running requests from blocking Odoo workers indefinitely.

Show example hardened odoo.conf values
[options]
admin_passwd = use_a_strong_random_secret_here

proxy_mode = True
list_db = False
dbfilter = ^your_odoo_database_name$

log_level = info

workers = 2
max_cron_threads = 1
limit_time_cpu = 600
limit_time_real = 1200

Adapt these values to the server size, Odoo version, and deployment method. The important point is to review them intentionally before exposing Odoo publicly.

Audit checklist

1. Check whether system updates are pending

Safe check

A VPS can already be outdated even if it was created recently. This step only checks whether updates or a reboot are pending. It does not upgrade packages or restart the server.

Show check and interpretation
Safe check

Command or manual check

sudo apt update
apt list --upgradable
[ -f /var/run/reboot-required ] && echo "Reboot required" || echo "No reboot flag found"

How to interpret the result

If packages are listed, updates are pending. Do not automatically upgrade a production Odoo server without planning the impact.

Pay special attention if the list includes:
- odoo
- postgresql
- nginx
- linux-image / linux-headers / kernel packages
- docker packages

These updates may restart services, change runtime behavior, or require a reboot.

If "Reboot required" appears, do not reboot a production Odoo server immediately. First:
- notify users if the server is used in production
- confirm you have working SSH or VPS console access
- confirm backups or snapshots exist
- schedule the restart during low-activity hours

For a demo or unused server, applying updates immediately may be acceptable, but it should still be a conscious decision.

2. Check which ports are listening

Safe check

Before connecting a domain, confirm that Odoo and PostgreSQL are not exposed directly to the internet.

Show check and interpretation
Safe check

Command or manual check

sudo ss -tulpn

How to interpret the result

Common public ports:
- 22 for SSH
- 80 for HTTP
- 443 for HTTPS

Odoo commonly uses:
- 8069 for the main Odoo HTTP service
- 8072 for websocket / longpolling traffic

Seeing port 8069 is not automatically a problem. What matters is the bind address.

Safer examples:
- 127.0.0.1:8069
- 127.0.0.1:8072

These mean Odoo is reachable locally by Nginx, but not directly exposed to the internet.

Risky examples:
- 0.0.0.0:8069
- 0.0.0.0:8072
- [::]:8069
- [::]:8072

These may mean Odoo is listening on public network interfaces.

Also investigate carefully if you see:
- 5432 exposed publicly for PostgreSQL
- 0.0.0.0:5432
- [::]:5432

If Odoo appears publicly exposed:
1. Do not ignore it just because the site works.
2. Confirm whether the firewall blocks direct access.
3. Check Docker port bindings with docker ps.
4. Prefer binding Odoo to 127.0.0.1 and exposing it only through Nginx.
5. Do not allow 8069, 8072, or 5432 publicly in the firewall.
6. Test from outside the server using http://your-server-ip:8069 after changes.

3. Check Docker port bindings

Safe check

Docker can publish ports directly even when the firewall looks restrictive. Always verify container bindings directly.

Show check and interpretation
Safe check

Command or manual check

docker ps --format "table {{.Names}}\t{{.Ports}}"

How to interpret the result

Prefer local-only bindings when Nginx runs on the host:
127.0.0.1:8069->8069/tcp
127.0.0.1:8072->8072/tcp

Avoid public bindings for Odoo:
0.0.0.0:8069->8069/tcp
0.0.0.0:8072->8072/tcp

Never expose PostgreSQL publicly:
0.0.0.0:5432->5432/tcp

If you see a public Odoo binding in Docker, update docker-compose.yml to bind Odoo to 127.0.0.1, then recreate the container.

Safer Docker Compose example:
ports:
  - "127.0.0.1:8069:8069"
  - "127.0.0.1:8072:8072"

Avoid:
ports:
  - "8069:8069"
  - "8072:8072"

The short form may publish the ports publicly depending on Docker defaults and host firewall behavior.

4. Check firewall status

Safe check

The firewall should only allow the ports required for administration and public web access.

Show check and interpretation
Safe check

Command or manual check

sudo ufw status verbose

How to interpret the result

Expected allowed ports:
- SSH
- 80/tcp
- 443/tcp

Odoo 8069/8072 and PostgreSQL 5432 should not be allowed publicly.

Important: if Docker is used, do not trust UFW alone. Docker can create iptables rules that publish ports even when UFW looks restrictive.

5. Plan firewall changes safely

Change required

Firewall changes can lock you out of your VPS. Always allow SSH before enabling or changing firewall rules.

Show check and interpretation
Change required

Command or manual check

# Example only — do not run blindly on production.
# Keep a second SSH session open before changing firewall rules.

sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw status verbose

How to interpret the result

Before enabling or changing UFW:
- keep a second SSH session open
- confirm OpenSSH is allowed
- confirm your VPS provider has an emergency console
- do not allow Odoo 8069/8072 publicly
- do not allow PostgreSQL 5432 publicly

Only enable or reload firewall rules when you understand the impact.

6. Locate the active Odoo configuration file

Safe check

Several important Odoo security settings live inside odoo.conf. Quick tutorials often skip them.

Show check and interpretation
Safe check

Command or manual check

sudo find / -name "odoo.conf" 2>/dev/null

How to interpret the result

You should locate the active Odoo config file.

Common locations:
- /etc/odoo/odoo.conf
- /etc/odoo.conf
- inside a Docker volume or mounted config folder

Do not edit it yet. First identify which config file the running Odoo service actually uses.

7. Review important odoo.conf settings

Caution

The goal is to review the current configuration before changing anything. Some values may contain secrets, so do not paste the full file publicly.

Show check and interpretation
Caution

Command or manual check

# Replace the path with your real odoo.conf path.
# Do not share the full output publicly if it contains secrets.

sudo grep -E "^(admin_passwd|proxy_mode|list_db|dbfilter|log_level|workers|max_cron_threads|limit_time_cpu|limit_time_real)" /path/to/odoo.conf

How to interpret the result

Important settings to review:
admin_passwd = should be strong and not default
proxy_mode = True when Odoo is behind a trusted reverse proxy
list_db = False for production-style single-database setups
dbfilter = should restrict which database is exposed
log_level = info or warn, not debug in production
workers = configured for production if needed
max_cron_threads = controlled
limit_time_cpu / limit_time_real = prevents long-running requests from hanging workers

Do not expose admin_passwd publicly.

8. Check whether the database manager is exposed

Caution

The Odoo database manager can be sensitive because it may allow database creation, duplication, backup, or restore depending on configuration.

Show check and interpretation
Caution

Command or manual check

# Local check when Odoo is bound to 127.0.0.1
curl -I http://127.0.0.1:8069/web/database/manager
curl -I http://127.0.0.1:8069/web/database/selector
curl -I http://127.0.0.1:8069/web/database/create

# Public check after Nginx/domain setup
curl -I https://your-odoo-domain.com/web/database/manager
curl -I https://your-odoo-domain.com/web/database/selector
curl -I https://your-odoo-domain.com/web/database/create

How to interpret the result

It is normal for Odoo database manager routes to exist locally.

Example local result:
- http://127.0.0.1:8069/web/database/manager may return 200 OK
- http://127.0.0.1:8069/web/database/selector may return 200 OK
- create/drop/backup routes may require POST or authentication

This is not automatically dangerous if Odoo is bound only to 127.0.0.1 and reachable only by the server or Nginx.

The real risk is public exposure.

Risky situations:
- Odoo is published on 0.0.0.0:8069
- Odoo is reachable from http://your-server-ip:8069
- Nginx proxies /web/database routes publicly without restriction
- PostgreSQL is also exposed publicly

Recommended protection layers:
- strong admin_passwd in odoo.conf
- list_db = False
- dbfilter configured to the intended database
- Nginx restriction for /web/database routes before exposing Odoo through HTTPS
- optional IP allowlist or basic auth for administrators
- no public PostgreSQL access

Sensitive routes to review:
- /web/database/manager
- /web/database/selector
- /web/database/create
- /web/database/drop
- /web/database/backup
- /web/database/restore

For many production deployments, blocking /web/database at the Nginx level is a sensible extra protection layer.

Target public result after Nginx hardening:
- /web/database/manager should return 403, 404, or be restricted
- /web/database/selector should return 403, 404, or be restricted
- /web/database/create should return 403, 404, or be restricted

9. Check PostgreSQL exposure

Safe check

PostgreSQL should only be reachable by Odoo or trusted internal services, not by the public internet.

Show check and interpretation
Safe check

Command or manual check

sudo ss -tulpn | grep 5432 || echo "No local PostgreSQL listener found in ss output"
docker ps --format "table {{.Names}}\t{{.Ports}}" | grep 5432 || echo "No Docker PostgreSQL public binding found in docker ps output"

How to interpret the result

Safer examples:
127.0.0.1:5432
Docker internal network only
No public binding shown

Risky example:
0.0.0.0:5432->5432/tcp

If PostgreSQL is public, treat it as urgent and plan a safe fix.

10. Review active sessions and recent logins

Safe check

Before exposing Odoo publicly, check whether the VPS already shows suspicious access or unknown users.

Show check and interpretation
Safe check

Command or manual check

who
last -a | head -30

How to interpret the result

You should recognize the active users and recent login history.

Unexpected users, unknown sessions, or unusual login origins should be investigated before continuing.

11. Review system users

Caution

Unexpected users can indicate old automation, previous admin accounts, or suspicious activity. A normal Linux server also has many system users, so the result needs careful interpretation.

Show check and interpretation
Caution

Command or manual check

# Full list of usernames
cut -d: -f1 /etc/passwd

# Better focused check: human-like users
awk -F: '$3 >= 1000 && $3 < 65534 {print $1, "uid="$3, "home="$6, "shell="$7}' /etc/passwd

# Users with interactive shells
grep -E "/bin/bash|/bin/sh" /etc/passwd

How to interpret the result

It is normal to see many system users such as:
- root
- daemon
- bin
- www-data
- sshd
- systemd-resolve
- _apt
- nobody
- postgres

Do not delete these users blindly. Many are required by Linux services, Nginx, PostgreSQL, SSH, package management, or systemd.

Usually normal on an Odoo/STYD server:
- your admin user, for example ubuntu, deploy, admin
- root, even if direct root SSH login is disabled
- postgres if PostgreSQL is installed on the host
- www-data if Nginx or web services are installed

Investigate carefully if you see:
- human-looking users you did not create
- old developer/admin accounts you no longer use
- users with /bin/bash that you do not recognize
- users with home folders under /home that you do not recognize
- recently created users after suspicious activity
- users with sudo access that you do not recognize

Useful follow-up checks:
- groups username
- sudo -l -U username
- ls -la /home
- last -a | head -30

If a user looks suspicious, first identify what owns it and whether a service depends on it. Do not delete accounts blindly on a production server.

12. Review failed and successful SSH login attempts

Safe check

Failed SSH login attempts are common on public VPS servers. The important question is not only whether bots are trying, but whether any login succeeded from an unknown IP, unknown user, or unsafe method.

Show check and interpretation
Safe check

Command or manual check

echo "=== Failed SSH attempts in last 24 hours ==="
(
  sudo journalctl -u ssh --since "24 hours ago" 2>/dev/null
  sudo journalctl -u sshd --since "24 hours ago" 2>/dev/null
  sudo cat /var/log/auth.log 2>/dev/null
) | grep -Ei "failed|invalid" || echo "No failed SSH attempts found in common SSH log locations"

echo "=== Successful SSH login summary ==="
(
  sudo journalctl -u ssh --since "24 hours ago" 2>/dev/null
  sudo journalctl -u sshd --since "24 hours ago" 2>/dev/null
  sudo cat /var/log/auth.log 2>/dev/null
) | grep -Ei "Accepted " \
  | sed -E 's/.*Accepted ([^ ]+) for ([^ ]+) from ([^ ]+).*/method=\1 user=\2 ip=\3/' \
  | sort \
  | uniq -c

echo "=== Recent login table ==="
last -a | head -30

echo "=== SSH hardening status ==="
sudo sshd -T | grep -Ei "permitrootlogin|passwordauthentication|pubkeyauthentication|kbdinteractiveauthentication|maxauthtries"

echo "=== Fail2ban SSH status ==="
sudo fail2ban-client status sshd 2>/dev/null || echo "Fail2ban sshd jail not found"

How to interpret the result

On some systems, SSH logs may be under the ssh service, the sshd service, or /var/log/auth.log. This checklist checks common SSH log locations, but if all outputs are empty, do not treat that alone as proof that there were no SSH attempts.

A few failed attempts can be normal. Many public VPS servers are scanned constantly.

Common bot usernames:
- root
- admin
- ubuntu
- test
- oracle
- postgres
- odoo
- odoo12
- deploy
- vagrant
- pi

Signs of brute-force scanning:
- many "Invalid user ..." lines
- repeated "Failed password for root"
- many different IP addresses
- many generic usernames you never created
- repeated attempts within minutes

How to check whether a brute-force attempt succeeded:
Look for successful login lines, especially:
- Accepted password for root from unknown-ip
- Accepted password for admin from unknown-ip
- Accepted password for any user from unknown-ip
- Accepted publickey for unknown-user from unknown-ip
- any successful login at a time you do not recognize

Good successful login example:
- method=publickey user=your-admin-user ip=your-known-ip

Risky successful login examples:
- method=password user=root ip=unknown-ip
- method=password user=admin ip=unknown-ip
- method=publickey user=unknown-user ip=unknown-ip

Good protection signs:
- permitrootlogin no
- passwordauthentication no
- pubkeyauthentication yes
- kbdinteractiveauthentication no
- Fail2ban sshd jail is active
- Fail2ban shows banned IPs

Recommended hardening:
- use SSH keys only
- disable root SSH login
- disable password authentication
- install or verify Fail2ban
- reduce MaxAuthTries, for example MaxAuthTries 3
- consider SSH IP allowlisting only if your IP is stable
- consider VPN-only SSH for stronger production environments

If a successful login from an unknown IP or unknown user is found, treat the server as possibly compromised.

Immediate response if suspicious successful login is found:
1. Do not ignore it.
2. Take a snapshot for investigation if possible.
3. Disable or lock the suspicious user.
4. Rotate SSH keys and passwords.
5. Check sudo users, cron jobs, systemd services, running processes, and recently modified files.
6. Check application secrets such as Odoo, PostgreSQL, API keys, .env files, and backup credentials.
7. Consider rebuilding the VPS from a clean snapshot if compromise is likely.

Do not simply change the password and continue if an attacker may have logged in. Once a server is compromised, credentials, backdoors, cron jobs, SSH keys, or malware may already have been added.

Fail2ban helps against repeated brute-force attempts, but it is not full DDoS protection. Large network floods must be handled by the hosting provider, upstream firewalling, or network-level protection.

13. Check cron jobs

Caution

Unexpected cron jobs can indicate unwanted scheduled scripts or old automation forgotten on the server.

Show check and interpretation
Caution

Command or manual check

crontab -l
sudo ls -la /etc/cron.d
sudo ls -la /etc/cron.daily
sudo ls -la /etc/cron.hourly

How to interpret the result

You should recognize the scheduled jobs.

Unknown scripts should be reviewed before exposing the server. Do not delete cron jobs blindly; first understand what they do.

14. Check disk, memory, and swap

Safe check

Odoo can fail in confusing ways when the server runs out of disk, memory, or swap.

Show check and interpretation
Safe check

Command or manual check

df -h
free -h

How to interpret the result

Disk should have enough free space for:
- Odoo database growth
- filestore
- logs
- backups
- Docker images and volumes

Memory should not be constantly exhausted.
Swap is recommended on small VPS instances.

Final pre-production verification

Before treating the server as production-ready, verify the setup from both the server side and the public internet side.

  • Only 22, 80, and 443 are publicly reachable from the VPS firewall.
  • Odoo 8069 is not publicly exposed.
  • Odoo websocket or longpolling port 8072 is not publicly exposed.
  • PostgreSQL 5432 is not publicly exposed.
  • Docker containers do not publish Odoo or PostgreSQL on 0.0.0.0.
  • HTTPS works and HTTP redirects to HTTPS.
  • proxy_mode is enabled only because Odoo is behind Nginx.
  • list_db is disabled after dbfilter is correctly configured.
  • The database manager routes may exist locally, but are blocked, restricted, or intentionally protected from the public internet.
  • The Odoo master password is strong and stored safely.
  • Backups include database, filestore, custom addons, and configuration files.
  • A restore path has been tested or documented before relying on the server.
  • No unknown successful SSH login appears in recent logs.
  • No hidden script, suspicious cron job, unknown SSH key, or abnormal high-CPU process remains unexplained.

Need help reviewing your Odoo VPS?

If you audited your server and found an exposed port, unknown login, suspicious process, Docker binding, PostgreSQL exposure, or Odoo setting you are not sure about, I can help review it.

You can send me sanitized command outputs such as ss -tulpn, ufw status, docker ps, or your Odoo/Nginx configuration with secrets removed.

Contact me for an Odoo VPS review →

Related guide

A secure Odoo server also needs a reliable backup and restore strategy. This guide only mentions backups as a pre-check. The full process is covered separately.

Odoo backups: database, filestore, and custom modules →