Cluster ERP has three small cryptographic helpers in src/cluster_erp/core/. core/totp.py computes time-based one-time passwords for the operator console. core/secret_box.py seals small control-plane secrets that must be read back, such as the operators' authenticator seeds. core/crypto_at_rest.py encrypts stored files and database dumps. This page states what each one protects and which setting supplies its key. It never shows key values; see the settings reference for the variable names.
In short¶
- The second factor is a standard time-based one-time password with six digits and 30-second steps, verified with a tolerance of one step on each side (
src/cluster_erp/core/totp.py:1-4,src/cluster_erp/core/totp.py:16-17,src/cluster_erp/core/totp.py:42-55). - In the code at this commit,
verify_totpis called only by the operator sign-in inserver/operators.py(src/cluster_erp/server/operators.py:129-168). - Operator seeds are stored sealed with the
enc:prefix; the key comes fromcontrol_encryption_key, or fromsecret_keywhen that setting is empty (src/cluster_erp/core/secret_box.py:1-9,src/cluster_erp/core/secret_box.py:33-37). - Files and backups use authenticated encryption with a 256-bit key given as 64 hexadecimal characters (
src/cluster_erp/core/crypto_at_rest.py:1-16,src/cluster_erp/core/crypto_at_rest.py:34-40). - In multi-tenant mode the settings validator refuses to start without
storage_encryption_key,backup_encryption_key, andmail_encryption_key(src/cluster_erp/config/settings.py:805-821).
How it works¶
The operator second factor¶
generate_secret creates a random 160-bit seed in base32 (src/cluster_erp/core/totp.py:20-22). otpauth_uri builds the enrolment link an authenticator app reads (src/cluster_erp/core/totp.py:58-63). verify_totp keeps only digits, requires six of them, and compares in constant time against the current step and its neighbours (src/cluster_erp/core/totp.py:42-55).
The operator account flow is in server/operators.py:
enroll_totpissues or replaces a seed, stores it sealed, stampstotp_enrolled_at, and returns the seed and link once (src/cluster_erp/server/operators.py:178-195). The commandpython -m cluster_erp.server.operators totp-enrollruns it (src/cluster_erp/server/operators.py:352-353).authenticatechecks the password first and the code second. A wrong code counts as a failed attempt. Whenvendor_require_totpis on, an operator without a seed cannot sign in (src/cluster_erp/server/operators.py:114-175,src/cluster_erp/config/settings.py:131).- A seed stored before sealing existed is sealed at its next successful use (
src/cluster_erp/server/operators.py:165-167).
The full operator sign-in is described in operator console.
Sealed control-plane secrets¶
secret_box uses Fernet from the cryptography package. The Fernet key is the SHA-256 digest of the configured material (src/cluster_erp/core/secret_box.py:27-37). seal returns enc: plus the token, and open_secret returns the plain value of a sealed or legacy unsealed entry (src/cluster_erp/core/secret_box.py:40-61). A changed key makes open_secret raise ValueError (src/cluster_erp/core/secret_box.py:58-61). The column control_operators.totp_secret is wide enough for the sealed form (src/cluster_erp/core/tenancy.py:147-148).
The mail module seals its own provider secrets with a separate helper keyed by mail_encryption_key (src/cluster_erp/modules/mail/crypto.py:1-10). The mail module page covers it.
Files and backups at rest¶
crypto_at_rest defines two containers (src/cluster_erp/core/crypto_at_rest.py:1-16):
| Container | Use | Key setting | Source |
|---|---|---|---|
| Blob | One stored attachment, encrypted in memory with a fresh random nonce | storage_encryption_key |
src/cluster_erp/core/crypto_at_rest.py:50-66, src/cluster_erp/core/storage.py:138-166 |
| Stream | A database dump of any size, encrypted in chunks that cannot be dropped, repeated, or reordered | backup_encryption_key |
src/cluster_erp/core/crypto_at_rest.py:70-140, src/cluster_erp/server/backup_tenants.py:241-320 |
EncryptedStorageBackend wraps any storage backend and passes the storage key as associated data, so a ciphertext copied to another key does not decrypt (src/cluster_erp/core/storage.py:138-160). Files written before a key existed are recognised by their missing header and read as plain data (src/cluster_erp/core/crypto_at_rest.py:14-15). parse_key refuses any key that is not 32 bytes of hexadecimal (src/cluster_erp/core/crypto_at_rest.py:34-40), and the settings validator applies it at start-up (src/cluster_erp/config/settings.py:822-827). The erasure ledger also seals each line with the backup key; see privacy and data-subject requests.
Why it is built this way¶
secret_boxexists because an authenticator seed must be read back to verify a code, so it cannot be hashed like a password (src/cluster_erp/core/secret_box.py:1-9).crypto_at_restuses authenticated encryption so that a modified byte fails to decrypt instead of producing wrong data, and takes the key from settings, never from the repository (src/cluster_erp/core/crypto_at_rest.py:3-5).parse_keyrefuses short keys because a short key accepted silently would be weaker than no key (src/cluster_erp/core/crypto_at_rest.py:35-36).- The settings validator demands a separate mail key in multi-tenant mode: one key, one purpose (
src/cluster_erp/config/settings.py:815-821).
Rules and boundaries¶
- Secret values never appear in change history: column names that contain
totpare in the credential list ofcore/sensitive_fields.py(src/cluster_erp/core/sensitive_fields.py:36). See change history. - Changing
control_encryption_keyorsecret_keyafter enrolment makes existing sealed seeds unreadable (src/cluster_erp/core/secret_box.py:58-61). Re-enrolment withtotp-enrollissues a new seed (src/cluster_erp/server/operators.py:178-195). - The control-plane retention pass anonymises a disabled operator account whose last sign-in is older than
control_actions_retention_yearsyears, and clears its password hash and seed (src/cluster_erp/server/control_retention.py:147-159).
Interaction with other parts¶
- Operator console uses
totpandsecret_box. - Attachments, notes, and storage uses the blob container through
EncryptedStorageBackend. - Backups and restore uses the stream container.
Limits and open points¶
- Tenant users have no second factor: a search for
verify_totpandtotpoversrc/cluster_erpfinds callers only inserver/operators.py,core/tenancy.py,server/control_retention.py, and the settings comment (src/cluster_erp/config/settings.py:130-131). - Key rotation tooling for
storage_encryption_keyis not determined from code beyond the module note thatre-encrypttooling can migrate legacy plaintext (src/cluster_erp/core/crypto_at_rest.py:14-15).
Terms introduced¶
| Term | Code identifier | Definition | Turkish term | Source |
|---|---|---|---|---|
| time-based one-time password | verify_totp |
A six-digit code derived from a shared seed and the current 30-second step. | zamana dayalı tek kullanımlık parola | src/cluster_erp/core/totp.py:42 |
| sealed secret | secret_box.seal |
A reversible, encrypted form of a small secret, marked with the enc: prefix. |
mühürlü gizli değer | src/cluster_erp/core/secret_box.py:44 |
| encryption at rest | encrypt_blob |
Authenticated encryption of a stored file or dump before it reaches disk or a bucket. | durağan şifreleme | src/cluster_erp/core/crypto_at_rest.py:50 |
Related pages¶
- Identity, access, tenancy, privacy, and licensing
- Authentication and sessions
- Operator console
- Settings reference
Verified against commit 92f8051 on 2026-11-13.