Odoo Security Hardening: Authentication, Access Rights, and Encryption
Enterprise ERP systems are high-value targets for cyberattacks. An Odoo instance containing customer PII, financial records, supplier contracts, and employee data represents a significant attack surface that demands deliberate, layered security controls.
This guide covers the complete security hardening process for Odoo 19 Enterprise: from authentication strengthening and access rights design through network hardening, encryption, audit logging, and incident response readiness.
Key Takeaways
- Default Odoo configurations are not production-ready from a security perspective
- Two-factor authentication (2FA) should be mandatory for all users with sensitive access
- Access rights follow the principle of least privilege — users get only what they need
- Record rules provide row-level security to enforce data isolation
- All inter-service communication must use TLS; never HTTP in production
- Database credentials must be rotated regularly and never hardcoded
- Audit logs must be tamper-proof and retained for compliance requirements
- Security patches should be applied within 48 hours of Odoo SA announcements
Authentication Hardening
Password Policy
Odoo 19 supports configurable password policies under Settings → Users & Companies → Password Reset Policy.
Recommended settings:
- Minimum length: 12 characters
- Require uppercase, lowercase, numbers, and special characters
- Password expiry: 90 days
- Prevent reuse of last 12 passwords
- Account lockout after 5 failed attempts (15-minute lockout)
# Custom password validator in a security module
from odoo import api, models
from odoo.exceptions import UserError
import re
class ResUsers(models.Model):
_inherit = 'res.users'
@api.constrains('password')
def _check_password_strength(self):
for user in self:
if user.password:
pwd = user.password
if len(pwd) < 12:
raise UserError("Password must be at least 12 characters.")
if not re.search(r'[A-Z]', pwd):
raise UserError("Password must contain an uppercase letter.")
if not re.search(r'[a-z]', pwd):
raise UserError("Password must contain a lowercase letter.")
if not re.search(r'\d', pwd):
raise UserError("Password must contain a number.")
if not re.search(r'[!@#$%^&*(),.?":{}|<>]', pwd):
raise UserError("Password must contain a special character.")
Two-Factor Authentication (2FA)
Enable 2FA under Settings → Permissions → Two-Factor Authentication.
Configuration options:
- Optional: Users choose whether to enable 2FA
- Required for specific groups: Apply to administrators and accountants
- Required for all users: Maximum security
For enterprise deployments, require 2FA for:
- Administrator accounts (
base.group_system) - Finance users (
account.group_account_manager) - HR managers with payroll access
- All users with API key access
OAuth and SSO Integration
For organizations with existing identity providers, configure Odoo to use SAML 2.0 or OAuth 2.0:
# In odoo.conf — configure OAuth
oauth_providers = [
{
'name': 'Your SSO Provider',
'client_id': 'odoo-client-id',
'auth_endpoint': 'https://sso.company.com/auth',
'validation_endpoint': 'https://sso.company.com/userinfo',
'scope': 'openid email profile',
}
]
This centralizes authentication in your identity provider, enabling features like single sign-on, centralized MFA, and automatic account deprovisioning when employees leave.
API Key Security
# Restrict API key creation to administrators only
# In custom security module:
# ir.rule to prevent non-admin users from creating API keys
<record id="rule_api_key_admin_only" model="ir.rule">
<field name="name">API Keys: Admin Only</field>
<field name="model_id" ref="base.model_auth_api_key"/>
<field name="domain_force">[('create_uid', '=', user.id)]</field>
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
<field name="perm_create" eval="False"/>
<field name="perm_unlink" eval="False"/>
</record>
Enforce a policy: API keys expire after 90 days, are tied to dedicated service accounts with minimal permissions, and are rotated on any suspected compromise.
Access Rights Architecture
Group Hierarchy Design
Odoo's access control is based on groups. Design a group hierarchy that reflects your organizational structure:
base.group_user (Internal User)
├── sales.group_sale_salesman (Salesperson)
│ └── sales.group_sale_manager (Sales Manager)
├── account.group_account_user (Accounting User)
│ └── account.group_account_manager (Accounting Manager)
├── stock.group_stock_user (Inventory User)
│ └── stock.group_stock_manager (Inventory Manager)
└── base.group_system (Administrator)
Principle of least privilege: Every user should have the minimum access required to perform their job. This limits the blast radius of compromised accounts.
Access Control Lists (ACL)
ACL entries in ir.model.access define model-level CRUD permissions:
id,name,model_id,group_id,perm_read,perm_write,perm_create,perm_unlink
# Salesperson: can read/write own orders, cannot delete
access_sale_order_salesman,sale.order.salesman,sale.model_sale_order,sales.group_sale_salesman,1,1,1,0
# Manager: full CRUD
access_sale_order_manager,sale.order.manager,sale.model_sale_order,sales.group_sale_manager,1,1,1,1
# Accounting user: read-only on sales orders
access_sale_order_accountant,sale.order.accountant,sale.model_sale_order,account.group_account_user,1,0,0,0
Record Rules (Row-Level Security)
Record rules filter which records a user can see within a model:
<!-- Salesperson sees only their own opportunities -->
<record id="rule_crm_salesman" model="ir.rule">
<field name="name">CRM: Own Leads</field>
<field name="model_id" ref="crm.model_crm_lead"/>
<field name="domain_force">
['|', ('user_id', '=', user.id), ('user_id', '=', False)]
</field>
<field name="groups" eval="[(4, ref('sales.group_sale_salesman'))]"/>
</record>
<!-- Multi-company isolation: users see only their company's records -->
<record id="rule_sale_order_company" model="ir.rule">
<field name="name">Sale Order: Company</field>
<field name="model_id" ref="sale.model_sale_order"/>
<field name="domain_force">
[('company_id', 'in', company_ids)]
</field>
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
</record>
Performance warning: Complex record rules with many search() or child_of operators can significantly slow down list views. Test all record rules against large datasets before deploying to production.
Network Security
TLS/HTTPS Enforcement
Never run Odoo over plain HTTP in production. All traffic must be encrypted.
Nginx TLS configuration:
server {
listen 80;
server_name your-odoo.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name your-odoo.com;
ssl_certificate /etc/ssl/certs/your-odoo.com.pem;
ssl_certificate_key /etc/ssl/private/your-odoo.com.key;
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_timeout 1d;
ssl_session_cache shared:MozSSL:10m;
ssl_session_tickets off;
# HSTS: 2 years, include subdomains
add_header Strict-Transport-Security
"max-age=63072000; includeSubDomains; preload" always;
# Security headers
add_header X-Frame-Options DENY always;
add_header X-Content-Type-Options nosniff always;
add_header Referrer-Policy strict-origin-when-cross-origin always;
add_header Permissions-Policy
"camera=(), microphone=(), geolocation=()" always;
}
Firewall Configuration
# UFW firewall rules for Odoo server
ufw default deny incoming
ufw default allow outgoing
# Allow SSH from management IP only
ufw allow from 10.0.1.100 to any port 22
# Allow HTTPS from anywhere
ufw allow 443/tcp
# Allow HTTP (for Let's Encrypt renewal)
ufw allow 80/tcp
# Allow PostgreSQL only from Odoo server (if separate DB server)
ufw allow from 10.0.1.10 to any port 5432
# Block direct access to Odoo port (must go through Nginx)
ufw deny 8069/tcp
ufw deny 8072/tcp
ufw enable
Rate Limiting
Configure rate limiting in Nginx to prevent brute force attacks:
# Rate limiting zones
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;
limit_req_zone $binary_remote_addr zone=api:10m rate=60r/m;
server {
# Apply rate limiting to login endpoint
location /web/login {
limit_req zone=login burst=3 nodelay;
limit_req_status 429;
proxy_pass http://odoo;
}
# Apply rate limiting to API
location /web/dataset/ {
limit_req zone=api burst=20 nodelay;
proxy_pass http://odoo;
}
}
Data Encryption
Database-Level Encryption
For highly sensitive data, Odoo fields can be encrypted at rest:
# Custom encrypted field using Fernet symmetric encryption
from cryptography.fernet import Fernet
from odoo import api, fields, models
import base64, os
class EncryptedField(models.Model):
_name = 'my.sensitive.model'
_fernet_key = os.environ.get('ODOO_FIELD_ENCRYPTION_KEY')
ssn_encrypted = fields.Char(string='SSN (Encrypted)', groups='hr.group_hr_user')
ssn_display = fields.Char(
string='SSN', compute='_compute_ssn_display',
inverse='_inverse_ssn_display'
)
def _get_fernet(self):
return Fernet(self._fernet_key.encode())
def _compute_ssn_display(self):
f = self._get_fernet()
for record in self:
if record.ssn_encrypted:
decrypted = f.decrypt(record.ssn_encrypted.encode()).decode()
record.ssn_display = f"***-**-{decrypted[-4:]}"
else:
record.ssn_display = False
def _inverse_ssn_display(self):
f = self._get_fernet()
for record in self:
if record.ssn_display:
encrypted = f.encrypt(record.ssn_display.encode()).decode()
record.ssn_encrypted = encrypted
Environment Variable Security
Never store secrets in odoo.conf in plaintext. Use environment variables:
# /etc/systemd/system/odoo.service
[Service]
Environment="ODOO_DB_PASSWORD=your_secure_password"
Environment="ODOO_ADMIN_PASSWD=your_master_password"
Environment="ODOO_FIELD_ENCRYPTION_KEY=your_fernet_key"
EnvironmentFile=/etc/odoo/secrets.env
# odoo.conf — reference env vars
[options]
db_password = ${ODOO_DB_PASSWORD}
admin_passwd = ${ODOO_ADMIN_PASSWD}
Audit Logging
Odoo's built-in audit logging tracks all model changes. Enable it for sensitive models:
# Enable audit logging via the Audit Trail module
# Or implement custom logging:
class AuditableMixin(models.AbstractModel):
_name = 'audit.mixin'
def write(self, vals):
# Log what changed before the write
for record in self:
old_vals = {f: record[f] for f in vals if f in record._fields}
self.env['audit.log'].create({
'model_name': self._name,
'record_id': record.id,
'user_id': self.env.uid,
'action': 'write',
'old_values': str(old_vals),
'new_values': str(vals),
'ip_address': self.env.context.get('remote_addr'),
})
return super().write(vals)
def unlink(self):
for record in self:
self.env['audit.log'].create({
'model_name': self._name,
'record_id': record.id,
'user_id': self.env.uid,
'action': 'unlink',
'old_values': str({f: record[f] for f in ['name', 'id']}),
})
return super().unlink()
Compliance-critical models to audit:
res.users(user creation, permission changes)account.move(invoice modifications)hr.payslip(payroll changes)res.partner(customer/vendor PII changes)ir.model.access,ir.rule(permission changes)ir.config_parameter(system configuration changes)
Security Patch Management
Subscribe to Odoo security advisories:
Odoo SA publishes security advisories at https://github.com/odoo/odoo/security/advisories. Subscribe to GitHub notifications for this repository.
Patch application process:
# Check current Odoo version
python3 odoo-bin --version
# Pull latest security patches (within a minor version)
cd /opt/odoo
git fetch origin
git log origin/19.0..HEAD --oneline # See what's changed
# Apply patches on staging first
git merge origin/19.0
python3 odoo-bin -d staging_db -u all --stop-after-init
# Test critical workflows on staging
# If all clear, apply to production during maintenance window
Target: patch within 48 hours of a Critical or High severity advisory. Medium severity: within 1 week. Low severity: next planned maintenance window.
Frequently Asked Questions
How do I audit who deleted a record in Odoo?
Odoo does not log deletions by default. Install the auditlog module from Odoo Community Association (OCA) or implement a custom unlink() override in your sensitive models. The OCA auditlog module logs create, write, and unlink operations to an auditlog.log model with user, timestamp, and field-level change tracking.
Can I restrict Odoo access by IP address?
Not natively in Odoo. Implement IP allowlisting at the Nginx or firewall level. For administrative access, require VPN connection as a prerequisite. Odoo's ir.config_parameter does support a web.base.url restriction, but true IP-based access control belongs in the network layer.
What is the difference between access rights (ACL) and record rules?
Access rights (ACL, stored in ir.model.access) control whether a user group can read, write, create, or delete any record of a model — this is model-level control. Record rules filter which specific records a user can access within a model using domain expressions — this is row-level control. Both work together: a user needs ACL read permission AND must pass the record rule domain filter to see a record.
How should I handle the Odoo master password (admin_passwd)?
The admin_passwd in odoo.conf controls access to the database management interface at /web/database/manager. In production: set it to a long random string (32+ characters), store it in a password manager, never share it, and consider disabling the database manager entirely with list_db = False in odoo.conf if you manage the database via direct PostgreSQL access.
Should I run Odoo as root?
Never. Create a dedicated odoo system user with no shell access and limited filesystem permissions. The odoo user needs read access to the Odoo codebase and write access only to the filestore and log directories. Run the systemd service as this user. Running as root violates the principle of least privilege and maximizes damage from any code execution vulnerability.
Next Steps
Security hardening is not a one-time task — it's an ongoing practice. New vulnerabilities emerge, user access requirements change, and compliance frameworks evolve. A secure Odoo deployment requires regular security reviews, patch management discipline, and access rights audits.
ECOSIRE provides Odoo security hardening assessments that identify misconfigurations, excessive permissions, missing controls, and compliance gaps. Our security team has experience with ISO 27001, SOC 2, and GDPR requirements in Odoo environments.
Request an Odoo Security Assessment from ECOSIRE →
We'll review your current configuration against industry standards and deliver a prioritized remediation plan with implementation support.
Written by
ECOSIRE TeamTechnical Writing
The ECOSIRE technical writing team covers Odoo ERP, Shopify eCommerce, AI agents, Power BI analytics, GoHighLevel automation, and enterprise software best practices. Our guides help businesses make informed technology decisions.
ECOSIRE
Transform Your Business with Odoo ERP
Expert Odoo implementation, customization, and support to streamline your operations.
Related Articles
Odoo vs NetSuite Mid-Market Comparison: Complete Buyer's Guide 2026
Odoo vs NetSuite for mid-market in 2026: feature-by-feature scoring, 5-year TCO for 50 users, implementation timelines, industry fit, and two-way migration guidance.
Tally to Odoo Migration 2026: Step-by-Step Guide for Indian SMBs
Tally to Odoo migration playbook for Indian SMBs in 2026: data model mapping, 12-step plan, GST handling, COA translation, parallel run, UAT, and cutover.
AI Fraud Detection for E-commerce: Protect Revenue Without Blocking Sales
Implement AI fraud detection that catches 95%+ of fraudulent transactions while keeping false positive rates under 2%. ML scoring, behavioral analysis, and ROI guide.