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 run docker inspect/ps on the host).

Namespace / network isolation

  • Kubernetes — each tenant gets its own namespace (saas-<subdomain>), labeled saas.dishonkadoh.com/managed=true and saas.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-network by default), isolated from each other only by the --db-filter restriction 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.