Local secrets for Odoo modules¶
AI-translated from Russian.
developer and server scenarios can pass arbitrary keys into the Odoo container via a fixed path /run/odpm/secrets.json (environment variable ODPM_SECRETS_PATH). In the ci scenario the host mount is disabled — secrets do not enter the image through this mechanism (see CI in Actions).
Why this exists¶
Integration modules often need API keys, tokens, and passwords for external services. They should not go in odpm.json or user_settings.json (often in git) and are awkward to duplicate in compose environment: (visible in docker compose config).
odpm provides a separate contract: you store values in .odpm/secrets.json on the host; in the container the module always reads one path.
Project files¶
| File | In git | Who writes | Purpose |
|---|---|---|---|
.odpm/secrets.example.json |
yes | odpm on init | key template without real values |
.odpm/secrets.json |
no | you or --secrets-file |
source — the only place to edit on the host |
.odpm/runtime/secrets.json |
no | odpm (secrets.materialize) |
runtime — mounted into the container |
On odpm --init, only secrets.example.json is copied. secrets.json is created manually, by copying from the example, or via CLI import.
Directory layout: project-layout. Generated artifacts: generated-files.
Format (schema v1)¶
{
"schema_version": 1,
"secrets": {
"payment_provider.api_key": "sk_test_...",
"custom_integration.token": "..."
}
}
schema_versionis required, only1secrets— object of string → string; keys are flat (often dotted:service.field)- Invalid JSON or types — error on import/materialize
Quick start¶
Option A: from template¶
cp .odpm/secrets.example.json .odpm/secrets.json
# edit values in .odpm/secrets.json
odpm --skip-start
docker compose up -d
Option B: import on init¶
odpm --init file:///path/to/developing-project \
--secrets-file /secure/vault/client-secrets.json
odpm --skip-start
Option C: import on existing project¶
odpm --secrets-file ~/Downloads/new-secrets.json --skip-start
docker compose up -d
Content is copied to .odpm/secrets.json with permissions 0600. The source file is not deleted.
Full cycle after changing secrets¶
- Edit
.odpm/secrets.json(or call--secrets-fileagain). - Run
odpm --skip-start— stepsecrets.materializeupdates.odpm/runtime/secrets.jsonand rebuildsdocker-compose.ymlif needed. - Restart the Odoo container:
docker compose up -d(or fullodpm). - Verify the mount (see below).
If secrets.json is deleted, on the next materialize stale .odpm/runtime/secrets.json is removed, and the volume and ODPM_SECRETS_PATH disappear from compose.
Verify the mount works¶
In docker-compose.yml for the odoo service (when source exists):
- variable
ODPM_SECRETS_PATH=/run/odpm/secrets.json - volume
.odpm/runtime/secrets.json:/run/odpm/secrets.json:ro,Z
Inside the container:
docker compose exec odoo test -f /run/odpm/secrets.json && echo OK
docker compose exec odoo python3 -c "import json; print(list(json.load(open('/run/odpm/secrets.json'))['secrets']))"
Use the second command for debugging only; do not log values in production.
Contract for Odoo module code¶
import json
from pathlib import Path
SECRETS_PATH = Path("/run/odpm/secrets.json")
def load_odpm_secrets() -> dict[str, str]:
if not SECRETS_PATH.is_file():
return {}
data = json.loads(SECRETS_PATH.read_text(encoding="utf-8"))
return dict(data.get("secrets") or {})
The module does not care about host paths — only /run/odpm/secrets.json. If the file is missing, return an empty dict or handle the error per module logic.
Typical usage: write to ir.config_parameter on module install, or read on each request (without caching secrets in logs).
Plan and materialize¶
- Prepare step
secrets.materialize(beforecompose.service) copies source → runtime. odpm planshows the step asupdate/noop/skip(in CI —skip).odpm plan --plan-show-difffor secrets — summary only (e.g. “will materialize 3 secret keys”), without values.
Example:
odpm plan --skip-start
odpm plan --skip-start --plan-show-diff
Security¶
.odpm/secrets.jsonis added to.odpm/.gitignoreautomatically on import/materialize.- If the file exists but is not in gitignore —
odpm planemits a warning. - Do not commit real values; keep
secrets.example.jsonin git withREPLACE_ME. - Do not put application secrets in
odpm.json,user_settings.json, or the project root.env(for structured keys). - More detail: security.
By scenario¶
| Scenario | Mount secrets | Notes |
|---|---|---|
developer |
yes | typical local dev |
server |
yes | deliver secrets.json to the VM (--secrets-file or copy) |
ci |
no | runtime config in the image; application secrets — separate deploy process |
CI (GitHub Actions, developer/server pipeline)¶
In the ci scenario (odpm --build-image) host secret mount is still disabled — secrets do not enter the image through this mechanism.
For a local developer/server pipeline in Actions, use ephemeral JSON and --secrets-file:
- name: Materialize module secrets for odpm project
run: |
cat > /tmp/odpm-ci-secrets.json <<'EOF'
{"schema_version":1,"secrets":{"payment.api_key":"${{ secrets.MODULE_PAYMENT_API_KEY }}"}}
EOF
chmod 600 /tmp/odpm-ci-secrets.json
odpm --secrets-file /tmp/odpm-ci-secrets.json --skip-start
working-directory: /path/to/odpm-project
The odpm repository verifies the contract in tests/test_ci_secrets_smoke.py (job compose-smoke in ci-docker.yml). Do not print secret values in logs.
Follow-up (Phase B, 4.4.1): bake secrets into the CI image with ODPM_BAKE_SECRETS=1 during odpm --build-image — see ADR-002.
CI image bake (ODPM_BAKE_SECRETS=1)¶
For the ci scenario, module secrets are not mounted from the host on docker compose up. For modules to see keys inside the image:
# 1. Ephemeral secrets on the runner (do not commit)
cat > /tmp/odpm-ci-secrets.json <<'EOF'
{"schema_version":1,"secrets":{"payment.api_key":"${{ secrets.MODULE_PAYMENT_API_KEY }}"}}
EOF
chmod 600 /tmp/odpm-ci-secrets.json
odpm --secrets-file /tmp/odpm-ci-secrets.json --skip-start
# 2. Bake into the image (explicit opt-in)
export ODPM_BAKE_SECRETS=1
odpm --build-image --image-tag myregistry/client-odoo:17.0
The image gets COPY runtime/secrets.json → /run/odpm/secrets.json and ENV ODPM_SECRETS_PATH. Without the flag or without .odpm/secrets.json, bake does not run.
Difference from .env and Odoo passwords¶
| Mechanism | Purpose |
|---|---|
.env |
ports, directories, ODPM_SCENARIO |
user_settings.json |
Odoo DB/admin passwords, modules, git |
.odpm/runtime/config.json |
odpm host→container service contract |
.odpm/secrets.json |
arbitrary keys for your modules (payment, SMTP API, …) |
Working as a team¶
- Fill
.odpm/secrets.example.jsonwith the list of required keys. - Each developer creates a local
.odpm/secrets.json(not in git). - When adding keys to the project:
cp .odpm/secrets.example.json .odpm/secrets.jsonand fill values, orodpm --secrets-file …from corporate vault.
Troubleshooting¶
| Symptom | What to check |
|---|---|
No /run/odpm/secrets.json in container |
Is .odpm/secrets.json present? Did you run odpm --skip-start? Scenario not ci? |
| Stale values after edit | odpm --skip-start + docker compose up -d |
| Schema error | schema_version: 1, all values are strings |
| Compose without volume | Remove source and regenerate compose; or add source and run odpm --skip-start again |
CLI flag: --secrets-file.