← Back to home
Your credentials, protected

How we protect your Odoo credentials.

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.

🔐

Encrypted at rest

Passwords stored as AES-128 + HMAC ciphertext. The database never sees plaintext.

🗝

Key kept separately

The master key lives in a managed secrets store. Never in the database, never in git.

👁

Read-only usage

We only read invoices and sales data. We never write back to your Odoo. Ever.

🧭

Isolated per account

Every API call scopes queries to your user_id, server-side. No cross-tenant access.

4
DB columns
URL, database, username, encrypted password. Nothing else.
0
Plaintext bytes
Your password is encrypted before it ever touches our disk.
2
Code paths
Connection-test endpoint and the sync worker. Both auditable.
Time to brute-force
AES-128 with HMAC. Realistically, no.
01

What we actually store

The full inventory

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.

  • Odoo URL — e.g. https://acme.odoo.com — used to reach your instance.
  • Database name — the Odoo database identifier — required by the Odoo RPC protocol.
  • Username — the login of the dedicated Odoo user you provision for us.
  • Encrypted password — a ciphertext blob. The original plaintext leaves your browser, is encrypted on our server, and is never written to disk in readable form.
Recommendation
Create a dedicated Odoo user for Vizualizer with read-only access to Sales and Accounting. Use an API key instead of a user password when possible. If you ever suspect a breach, rotate that single credential — your real admin account is untouched.
02

What the database row actually looks like

Schema, not prose

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:

  • The password column stores ciphertext only — the plaintext never exists on our infrastructure after encryption.
  • Ciphertext without the master key is cryptographically useless — no dictionary, no brute force in any feasible timeline.
  • Each row is bound to a user_id. Our API refuses to return a connection that does not belong to the caller.
03

The encryption, in technical detail

Symmetric authenticated encryption

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.

Cipher
AES-128-CBC
Integrity
HMAC-SHA256
Library
cryptography.Fernet
Nonce / IV
Random per encrypt

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()
Why it matters
AES-128-CBC provides confidentiality; the HMAC-SHA256 tag detects tampering. Flip a single byte of a stored ciphertext and decryption fails loudly instead of silently producing garbage. This is authenticated encryption — the modern standard.
04

Where the master key lives

Separation of secret and data

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.

  • Injected into the server process at boot from a managed secrets store. It never touches the application database.
  • Never committed to git, never written to application logs, never returned by any API endpoint.
  • Only the FastAPI server and the Celery sync worker hold it in memory, and only for the lifetime of that process.
  • Database backups, read replicas, and staging environments use different data and cannot decrypt production ciphertext.
The practical consequence
An attacker who steals a database dump — the most common breach scenario — gets rows of opaque ciphertext. They do not get your password. They do not get anything they can feed into Odoo. They get noise.
05

How data moves on the network

Encryption in transit

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.

your browserour API
HTTPS · TLS 1.2+
our workeryour Odoo
HTTPS · TLS 1.2+

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.

06

Who can read your credentials

Access boundaries

The set of processes and people that can decrypt a stored password is deliberately tiny.

  • Application code paths: exactly two — the connection-test endpoint and the background sync worker. Both live in the repository. Both are auditable.
  • Humans: no Vizualizer team member has a routine workflow that calls decrypt(). Direct DB access is audited and used for migrations, not row inspection.
  • Other tenants: cannot. Every query is scoped by user_id, enforced server-side from the JWT, not by the frontend.
No admin "read password" button
We have intentionally not built a UI or backdoor that displays a stored password in plaintext. Adding one would require shipping code — and you would see it in the change history.
07

Read-only by design

We look, we never write

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.

  • No write, unlink, or create calls are ever issued against your Odoo.
  • No automation, no workflow triggers, no side effects on your business data.
  • You can further restrict by giving Vizualizer's Odoo user read-only access groups — then even a bug or compromise on our side cannot alter your data.
08

Threat model — when things go wrong

Realistic scenarios, stated plainly

Security promises are only meaningful when measured against specific threats. Here is how each common scenario plays out:

Scenario
Outcome
Database leak
Attacker gets encrypted ciphertext only. The master key is not in the database. Passwords cannot be recovered.
Log or APM leak
Passwords are never logged, in plaintext or otherwise. Request bodies containing credentials are filtered before reaching observability systems.
Malicious employee
Production secret-store access is limited and audited. There is no admin UI that reveals plaintext — accessing them requires shipping new code, which is reviewable.
Man-in-the-middle
All traffic uses TLS 1.2+ with modern cipher suites. Certificate validation is strict; downgraded connections are refused.
Backup compromise
Backups contain the same encrypted ciphertext. The master key is stored separately and is not included in database backups.
09

When you delete a connection

Right to erasure

Click the × next to a connection in the sidebar and the row — including the encrypted password — is deleted from our database immediately.

  • The row is removed by a hard DELETE, not a soft flag.
  • Daily database backups roll off within our retention window; after that, no copy of the ciphertext remains anywhere.
  • Synced invoice data tied to that connection is removed in the same transaction.
Even stronger
If you rotate the password inside Odoo after disconnecting, the old ciphertext — even if it somehow survived — would decrypt to a password that no longer opens any door.
10

Questions, audits, disclosure

Talk to a human

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