Security & Encryption
How the master stores credentials, which keys exist, how to rotate them, and what limits are enforced on remote command execution.
1. The Two Keys
The master uses two independent Fernet keys. They serve different purposes and must not be confused.
| Field key | Transport key | |
|---|---|---|
| System parameter | sadeem_saas_field_key |
sadeem_saas_secret_key |
| File alternative | sadeem_saas_field_key_path |
sadeem_saas_secret_key_path |
| Protects | Credentials stored on the master: server passwords, SSH private keys, API keys, tokens | Payloads sent to the instances (database names, login tokens, timestamps) on SaaS Slave route calls |
| Shared with instances | No — never leaves the master | Yes — every slave instance holds a copy |
| Rotatable from the UI | Yes (see below) | No — rotating it means re-deploying the key to every instance |
Why they are separate
The transport key is copied into every customer instance, so it is the more exposed of the two. Splitting the keys means a leak of the transport key does not expose the credentials stored on the master.
Key resolution order
For each key, the _path parameter wins when it is set: the file's first line is read and used. If no path is configured, the plain system parameter value is used.
sadeem_saas_field_key_path (file) → sadeem_saas_field_key (parameter)
Databases created before the key split
If sadeem_saas_field_key is not configured at all, stored credentials fall back to the transport key. When both keys exist, the master encrypts with the field key and decrypts with either, so values written under the old scheme keep working and are upgraded whenever they are rewritten.
2. Which Fields Are Encrypted
Each addon declares the encrypted fields of its own models in a class-level _ENCRYPTED_FIELDS list. There is no central inventory: the effective list for a model is the union of the declarations across its whole class hierarchy, so only installed addons contribute.
class SadeemRootServer(models.Model):
_name = 'sadeem.root.server'
_ENCRYPTED_FIELDS = ['password', 'code_server_password']
Declared in sadeem_saas_base:
| Model | Encrypted fields |
|---|---|
sadeem.root.server |
password, code_server_password |
sadeem.postgresql.server |
password |
sadeem.saas.backup.server |
api_key |
sadeem.ntfy.server |
password, admin_password, fcm_server_key, auth_token |
Other addons (Cloudflare, Nginx Proxy Manager, Portainer, Git accounts, subscriptions) add their own declarations the same way.
Values are stored as Fernet tokens, recognisable by their gAAAAA prefix. Helpers live in sadeem_saas_base/utils/utils.py:
| Helper | Purpose |
|---|---|
encrypt_field(env, value) |
Encrypt with the field key; returns the value unchanged if it is already a token |
decrypt_field(env, value) |
Decrypt with the field key (falling back to the legacy transport key) |
encryption_actions(env, data, encrypt=True) |
Encrypt/decrypt with the transport key — used for slave route payloads |
model_encrypted_fields(model) |
Union of _ENCRYPTED_FIELDS across the model's MRO |
Plaintext sweep
The daily cron Sadeem: Encrypt Plaintext Sensitive Fields walks every model that declares _ENCRYPTED_FIELDS and encrypts any value still stored in plaintext. It is idempotent — already-encrypted values are skipped — and deactivates itself once every record is clean.
3. Rotating the Field Encryption Key
Settings → SAAS Manager → SAAS Base Configuration → Rotate Field Encryption Key (visible to base.group_system only).
The wizard generates a new key, re-encrypts every stored credential with it, and stores the key where the configuration expects it.
| Result field | Meaning |
|---|---|
| New Key | The generated key — shown once, right after the rotation |
| Stored In | key file <path> or system parameter "sadeem_saas_field_key" |
| Re-encrypted Values | Number of values successfully re-encrypted |
| Skipped Values | Values that could not be decrypted with any known key; left unchanged, details in the server log |
| Storage Warning | Present only when the key file could not be written (see below) |
Back up first
Back up the database — and the current key file if you use sadeem_saas_field_key_path — before rotating. The new key is displayed only once; losing it means losing every stored credential.
Unwritable key file
If sadeem_saas_field_key_path points at a file that cannot be written, the database has already been re-encrypted, so the rotation must still finish consistently: the key falls back to the sadeem_saas_field_key parameter and the stale path setting is cleared. To return to file mode — write the displayed key into the file, set sadeem_saas_field_key_path again, then delete the sadeem_saas_field_key parameter.
Rotation touches only credentials stored on the master. The transport key shared with the instances is not affected, so no instance needs to be redeployed.
What "skipped" means
A skipped value is a token that no current key can decrypt — typically a leftover from a key rotated away long ago (for example stale chatter tracking values). It is left byte-for-byte unchanged so nothing is destroyed; check the server log for the exact table, column and record id.
4. Access Groups
Three groups are defined under the Sadeem SAAS category, each implying the previous one:
| Group | Typical rights |
|---|---|
| User | Read-only access to the SaaS records they are allowed to see |
| Operator | Day-to-day operation: read/write on subscriptions, migrations and infrastructure records |
| Administrator | Full create/delete, plus the destructive actions (migrate, cancel, key rotation is base.group_system) |
Record rules further restrict root servers, backup servers and Git accounts, and every shared record model carries a multi-company rule.
SSH is manager-only
Executing SSH commands on a root server requires the SaaS manager group — Only SaaS managers can execute SSH commands.
5. Remote Command Execution Limits
Two different execution paths exist, with different trust levels.
Root server SSH terminal (`sadeem_saas_base`)
Runs the typed command over SSH on the root server. Manager-only, every execution is written to the action log (Input Text / Output Text / Action Type) for audit.
Container command execution (`sadeem_saas_docker`)
Commands typed for a subscription's container are checked by is_safe_command() before they are sent:
- Any command containing
;,&or|is rejected outright — no chaining, no pipes - The base command must be in the whitelist, and every argument must be in that command's allowed list
| Command | Allowed forms |
|---|---|
ls |
ls, -l, -a |
apt |
apt update, apt upgrade, apt install python3-<package>, apt remove … |
pip / pip3 |
pip install <package>, pip uninstall <package> |
Package names are validated separately, so pip install evil;rm -rf / never reaches the container.
Elevated privileges
Commands run inside the container with elevated privileges, and root servers may have sudo enabled. Always take a backup before running anything that changes the system.
6. Public Endpoints and Their Secrets
| Route | Authentication |
|---|---|
/sadeem/webhook/<token> |
64-character token in the URL, unique per webhook record |
/sadeem/migration/restore/<token> |
Single-use restore callback token, rotated on every restore request; accepted only while the migration is in state Restoring |
Slave routes (/sadeem/database/*, …) |
Payloads encrypted with the transport key; the instance decrypts them to verify the caller |
See Webhooks, Server Migration and SaaS Slave for the full contracts.