Security¶
Tenant isolation is enforced at several layers, all inside
saas.provisioner.
Database isolation¶
Every Odoo container/pod is started with:
--db-filter ^<database_name>$
--no-database-list
This restricts the process to exactly one database and disables Odoo’s database selector/manager UI entirely — a tenant’s container can never even list, let alone touch, another tenant’s database, regardless of anything that happens at the application layer above it.
Per-tenant master password¶
Each tenant gets its own randomly generated admin_passwd
(secrets.token_urlsafe(32)), written into an odoo.conf the container
mounts read-only:
Kubernetes — stored as a Kubernetes Secret (
odoo-security-<subdomain>), mounted at/etc/odoo/odoo.conf. Created once; not regenerated on redeploys so the master password stays stable across restarts/reprovisions.Docker — written to a host-side config file under
DOCKER_TENANT_CONFIG_BASE_PATH/<subdomain>/odoo.conf,chmod 600, owned by the Odoo UID/GID, and bind-mounted read-only into the container. The password is written straight to that file and never passed as a container command-line argument (command-line args are visible to anything that can rundocker inspect/pson the host).
Namespace / network isolation¶
Kubernetes — each tenant gets its own namespace (
saas-<subdomain>), labeledsaas.dishonkadoh.com/managed=trueandsaas.dishonkadoh.com/instance=<id>for auditing. Pods run as a fixed non-root UID/GID (100/101) via a pod security context (run_as_user/run_as_group/fs_group).Docker — all tenant containers share one bridge network (
saas-networkby default), isolated from each other only by the--db-filterrestriction above rather than network segmentation — worth knowing if you’re reasoning about the Docker backend’s isolation properties versus the Kubernetes one.
Storage isolation¶
Filestore and custom-addons live on separate volumes per tenant (two PVCs on Kubernetes, two bind-mounted directories on Docker) — deliberately split so application code and tenant-generated data have independent lifecycles. See Architecture § Custom addons for why addon code and Odoo data are never mixed on the same volume.
Command redaction in logs¶
Any pod-exec/container-exec command containing --db_password or
--password has the following argument redacted before being logged
(_redact_command) — worth knowing if you’re grepping provisioning logs
for a failed command and the password shows up as ***REDACTED*** instead
of the real value; that’s intentional.
Custom addon deployment path traversal checks¶
Before any addon is deployed via the Git workflow, its technical name and
addon path go through explicit validation
(deploy_custom_module_to_instance / deploy_addon_to_instance):
technical names must match ^[A-Za-z0-9_][A-Za-z0-9_.-]*$, and the
resolved addon path is checked against ../, ./, and any path-escape
attempt relative to the cloned repository root before anything is copied
into tenant storage.