OBM's normal path is agentless: it calls Odoo's own
/web/database/backup endpoint over HTTPS and nothing is installed on your
servers. Some hardened instances make that impossible. For those, the
Agency plan can log in over SSH and stream a pg_dump instead.
This page is the whole feature: when to reach for it, how to set it up safely, and what
changes about your restores if you do.
Why this exists
Odoo 17 and later check the requested database against the database listing before
dumping it. That means an instance configured with list_db = False — the standard
hardening advice for a production Odoo — cannot be backed up over HTTP at all. The endpoint
doesn't merely hide the database picker; it refuses the backup, and it refuses it with the
same generic error page Odoo returns for a wrong master password.
Until now the only honest answer was "turn list_db back on, or back that server
up out of band". SSH backups are the third option: OBM connects to the Odoo host as a user you
nominate, runs pg_dump there, and streams the result straight back — no
list_db, no database manager, no agent to install or patch.
What you get, and what you give up
This is the part to read twice, because it changes what a restore looks like.
- An SSH backup is always a
pg_dump -Fccustom-format dump. Not a zip. The per-database "include filestore" setting is ignored on an SSH instance, because the filestore simply isn't reachable over this channel — OBM asks PostgreSQL for the database and nothing else. - No filestore means no attachments. Everything in the database is there:
records, settings, and any attachment Odoo was configured to keep in the database.
Files that live on disk under
filestore/— most attachments, website assets — are not. If you need those, back them up alongside OBM (rsync, restic, a snapshot of the volume), or use the HTTP path on an instance where you can. - Restores still go over HTTP. This surprises people, and it's the good kind
of surprise: Odoo's
/web/database/restoreendpoint works fine withlist_db = False— only backup is gated on the listing. So one-click restores keep working on SSH instances, and OBM keeps using the master password for them. Register the instance's real master password even if every backup goes over SSH. - Verification differs. A zip is verified by CRC-checking every member and
reading its manifest; a dump is verified by its
PGDMPheader. Both record a SHA-256. See troubleshooting for what a failure means. - Discovery won't work. Live database discovery uses the same listing endpoint Odoo is refusing, so it will report the listing as disabled. Type the database names in by hand when you add them — that's the expected flow here, not a bug.
1. Generate a dedicated key pair
Do not reuse your own key, and do not reuse a deploy key. OBM should hold a credential that does exactly one job and can be revoked without disturbing anything else.
# on your own machine — a key pair used by nothing but OBM
ssh-keygen -t ed25519 -C "obm-backup" -f ./obm_backup -N ""
# ./obm_backup -> the PRIVATE key: paste this into OBM
# ./obm_backup.pub -> the PUBLIC key: install this on the Odoo host The private key is stored encrypted at rest and is write-only: once saved, the API will never hand it back — not to you, not to anyone. Rotating means pasting a new one.
2. Install the public key on the Odoo host
# on the Odoo host, as the user OBM will log in as (usually: odoo)
install -d -m 700 ~/.ssh
cat >> ~/.ssh/authorized_keys <<'KEY'
restrict,from="203.0.113.10" ssh-ed25519 AAAAC3Nza... obm-backup
KEY
chmod 600 ~/.ssh/authorized_keys The two options in front of the key are the least-privilege part, and both are worth keeping:
-
restrictdisables port forwarding, agent forwarding, X11 and pty allocation. OBM needs none of them. -
from="…"limits the key to the address that backups come from. Ask us for the current egress address of the worker fleet before you set this — and remember to update it if we tell you it changed.
OBM runs exactly two commands over this connection, and never anything else:
true # test-connection, and the TOFU probe before a dump
pg_dump -Fc -d 'mydb' # the backup itself — stdout is streamed to OBM
The database name is validated against a strict pattern and shell-quoted before it reaches the
command line, so a database name can never turn into a second command. If you want to go
further and force a wrapper script with
command="…" in authorized_keys, it must honour
$SSH_ORIGINAL_COMMAND — both of the above have to keep working, including the
plain true.
3. Make sure the SSH user can actually run pg_dump
This is the requirement people miss, and it is the one that produces the ugliest error. OBM
runs pg_dump -Fc -d <database> and nothing more: no host, no user, no
password. Everything about authenticating to PostgreSQL has to already be true for that user,
on that host. Prove it before you touch OBM:
# log in exactly as OBM will, and prove pg_dump works with no arguments
# beyond the database name — no -h, no -U, no password prompt
ssh -i ./obm_backup [email protected] 'pg_dump -Fc -d mydb | wc -c'
# a number in the tens of megabytes = you are done
# "fe_sendauth: no password supplied" or "role ... does not exist" = not yet
The usual reason this works out of the box is peer authentication: you log in
as the odoo unix user, PostgreSQL is on the same machine, and the unix account
maps to a PostgreSQL role that owns the database.
# /etc/postgresql/16/main/pg_hba.conf — the line that makes it work
local all all peer
# ... and the unix user must map to a postgres role that can read the db:
sudo -u postgres createuser --superuser odoo # if it doesn't exist yet If PostgreSQL lives elsewhere or wants a password, use a .pgpass file:
# alternative when Postgres is on another host / uses password auth
# ~/.pgpass on the Odoo host, owned by the SSH user, mode 0600
db.internal:5432:*:odoo:the-password
# PGHOST/PGUSER must reach a NON-INTERACTIVE ssh command, so set them where
# one is actually read — e.g. ~/.bashrc (bash sources it for sshd-run
# commands) or ~/.ssh/environment with PermitUserEnvironment=yes in sshd_config
Also check that pg_dump is on the PATH of a non-interactive SSH
command — ssh host 'command -v pg_dump' answers that in one line — and that its
version is at least as new as the server it dumps.
4. Enable SSH on the instance and test the connection
On the instance, switch on SSH backups and fill in the host, the port (22 unless you moved it), the username, and the private key. All four are required together: saving a partial configuration is refused with "SSH backups need a host, a username and a private key", and enabling the feature at all on a non-Agency plan is refused with "SSH backups require the Agency plan".
Then press Test connection. This is not optional politeness — it is how the server's identity gets recorded.
Host-key pinning (trust on first use)
OBM does not consult a known_hosts file. It keeps exactly one trusted fingerprint
per instance and enforces it itself.
- First successful test-connection pins the key. Reopen the instance
afterwards and it shows Pinned host key: SHA256:…. Compare that string against the
host's real fingerprint the first time —
ssh-keyscan -t ed25519 host | ssh-keygen -lf -, run somewhere you trust. That comparison is the entire security value of trust-on-first-use; skipping it means trusting whatever answered. - Every later connection must match. A mismatch aborts immediately with "SSH host key changed — re-verify the server and re-run test-connection to re-pin", and the backup fails. It is never retried automatically, because a changed host key is either a rebuild you know about or an attack you need to know about — a machine shouldn't guess which.
- Changing the host or the username clears the pin and the next successful connection re-pins. Changing the port or rotating the key does not: same server, same identity.
- To re-pin after a legitimate rebuild: verify the new fingerprint out of band first. If the host or username moved at all, the pin is already gone — just run test-connection again. If the server came back at the same address under the same username, there is no self-service "trust the new key" button; e-mail support and we will clear the pin.
If a backup is the first thing that ever connects — you enabled SSH and a schedule fired before you tested — the worker learns the fingerprint, pins its own dump connection to it so both connections have to agree, and reports it back to be recorded. It still works; you just skipped your chance to check the fingerprint by hand.
Errors you may see, and what each one means
- "SSH backups require the Agency plan" — the feature is Agency-only. An org that downgrades keeps its SSH configuration, but new SSH backups are refused rather than quietly falling back to HTTP.
- "SSH backups need a host, a username and a private key" — a partial configuration was submitted. Fill in all three.
- "the stored SSH private key could not be parsed" — the pasted key isn't a
key OpenSSH would accept. Paste the whole file including the
-----BEGIN…and-----END…lines, and make sure it is the private key, not the.pub. - "SSH authentication failed (key rejected by the server)" — the server
answered but refused the key. Check the public key really is in that user's
authorized_keys, that~/.sshis mode 700 and the file 600, that the account isn't locked, and that afrom="…"restriction isn't excluding us. This one is retried automatically, so it will fail a few times before it settles. - "cannot reach SSH host (…)" — nothing answered: wrong host or port, a
firewall, or the machine is down. The class name in the parentheses
(
ConnectionRefusedError,TimeoutError, …) tells you which. Retried automatically. - "SSH host key changed — re-verify the server and re-run test-connection to re-pin" — the pin didn't match. See above. Never retried.
- "SSH connected but could not execute commands" — the login works but the
shell doesn't run anything. Usually a forced command in
authorized_keys, or a shell set to/usr/sbin/nologin. - "pg_dump exited with status 1: …" — the real PostgreSQL error follows the
colon, truncated. "role does not exist" or
"no password supplied" means step 3 isn't done. "database does not exist"
means the registered database name doesn't match what is on the server — likely a typo,
since discovery can't fill it in for you. "server version mismatch" means the host's
pg_dumpis older than its PostgreSQL. - "ssh dump timed out after 3600s" — the dump exceeded the transfer timeout. See the timeout entry in troubleshooting.
- "ssh dump failed mid-stream (…)" — the connection died partway. The partial file is discarded, never stored; nothing half-written is ever recorded as a backup.
- "verification failed: missing PGDMP header — not a pg_dump custom-format archive" — bytes arrived, but they aren't a dump. Almost always a shell profile on the Odoo host printing a banner into stdout, which corrupts the stream. Make the SSH user's startup files silent for non-interactive sessions.
Restoring an SSH backup
One click from the dashboard, into any instance you have registered — the upload goes to
Odoo's /web/database/restore, which accepts a custom-format dump as happily as a
zip. What you get back is the database only: expect missing attachments until you restore the
filestore separately.
The manual path is the standard one, and needs nothing from OBM:
# what OBM stores is a plain custom-format dump, so the manual path is:
createdb -O odoo mydb_restored
pg_restore --no-owner --role=odoo -d mydb_restored mydb-20260728.dump Both paths are covered in full in Restore a backup, including what to do when a proxy rejects a large upload.
Next steps
- Troubleshooting — symptom, cause and fix for the failures that actually happen.
- Security, in plain language — how the key, the master password and the archives are handled.
- Get started with OBM Pro — the rest of the hosted platform, from onboarding to metrics.