We connect to your Odoo to sync invoices. That means we hold a password. This page explains — in plain English first, in technical detail second — exactly how we store it, who can read it, and why a stolen database snapshot would be useless to an attacker. Last reviewed with every release.
Passwords stored as AES-128 + HMAC ciphertext. The database never sees plaintext.
The master key lives in a managed secrets store. Never in the database, never in git.
We only read invoices and sales data. We never write back to your Odoo. Ever.
Every API call scopes queries to your user_id, server-side. No cross-tenant access.
When you connect an Odoo instance, only these fields are persisted in our database. Nothing else — no cookies, no session tokens, no Odoo admin keys, no analytics data beyond what is needed to sync your invoices.
Here is the real shape of the table that holds your connection. Notice what is absent: no plaintext_password column, no password_hint, no recovery field.
# simplified shape of what sits in the database odoo_connections ( id SERIAL PRIMARY KEY, user_id INTEGER, url TEXT, -- e.g. https://acme.odoo.com database TEXT, -- e.g. acme-prod username TEXT, -- e.g. bot@acme.com encrypted_password TEXT, -- ciphertext only — never plaintext ... )
Three things make this row safe to hold:
We use Fernet, the encryption scheme from the well-audited Python cryptography library. Fernet is not a home-grown protocol — it is a specification built on top of primitives reviewed for decades.
Every password you save goes through the same two functions. That is literally all the code there is — no custom crypto, no shortcuts:
# backend/app/core/security.py (excerpt) from cryptography.fernet import Fernet def encrypt(value: str) -> str: return get_fernet().encrypt(value.encode()).decode() def decrypt(value: str) -> str: return get_fernet().decrypt(value.encode()).decode()
Encryption is only as strong as key management. The master key — the one that can turn ciphertext back into your password — is deliberately kept far away from the data it protects.
Your password makes exactly two hops when you save a connection, and exactly one every time we sync. Both hops are protected by TLS with modern cipher suites.
We never accept credentials over an unencrypted connection. The password is decrypted into memory only for the duration of a single RPC call to your Odoo, then dropped.
The set of processes and people that can decrypt a stored password is deliberately tiny.
Even with valid credentials, our code cannot modify your Odoo. The sync worker calls only read methods — search_read, read — against invoice, partner, and product models.
Security promises are only meaningful when measured against specific threats. Here is how each common scenario plays out:
Click the × next to a connection in the sidebar and the row — including the encrypted password — is deleted from our database immediately.
We welcome security reviews. If you are a security researcher, a CISO evaluating the product, or a customer wanting to run your own audit, we will work with you.
Report a vulnerability, ask for architecture details, or request a pentest window: security@hazenfield.com