Encrypted Storage#
RAGFlow provides optional encryption-at-rest for all object storage backends via EncryptedStorageWrapper , a transparent proxy that encrypts on put and decrypts on get without changing any calling code.
Architecture#
Caller → EncryptedStorageWrapper → (MinIO / S3 / Azure / OSS / GCS / OpenDAL)
↓
CryptoUtil
(AES-256-CBC default)
EncryptedStorageWrapper wraps any storage backend and validates that it exposes five required methods: put, get, rm, obj_exist, and health. Optional methods (get_presigned_url, copy, move, scan, bucket_exists, remove_bucket) are forwarded only when present . The rm, obj_exist, and health calls pass through directly without encryption/decryption .
Magic Header & Backward Compatibility#
Every encrypted blob is prepended with the 4-byte magic header b"RAGF" . On get, the decrypt() method checks for this header before attempting decryption:
- Header present → strip header, extract the embedded IV (next 16 bytes), decrypt ciphertext, remove PKCS7 padding.
- Header absent → data is plaintext; return it as-is.
This makes the wrapper safe to deploy over existing buckets containing unencrypted objects — legacy files are read correctly without migration.
Wire Format#
Encrypted blobs have the layout:
| 4 bytes: "RAGF" | 16 bytes: random IV | N bytes: AES-CBC ciphertext (PKCS7-padded) |
Each put generates a fresh random IV via os.urandom(16) . The key is derived from the raw RAGFLOW_CRYPTO_KEY string using PBKDF2-HMAC-SHA256 with a fixed salt (ragflow_crypto_salt, 100,000 iterations) so the derived key is always the same for a given passphrase .
Supported Algorithms#
Configured via RAGFLOW_CRYPTO_ALGORITHM :
| Value | Class | Key size |
|---|---|---|
aes-256-cbc (default) | AES256CBC | 32 bytes |
aes-128-cbc | AES128CBC | 16 bytes |
sm4-cbc | SM4CBC | 16 bytes |
All three are implemented in common/crypto_utils.py using the cryptography library.
Initialization & Fallback#
Encryption is wired in init_settings():
- The underlying storage backend is always instantiated first via
StorageFactory.create(). - If
RAGFLOW_CRYPTO_ENABLED=true,create_encrypted_storage()wraps it withEncryptedStorageWrapper. - On any exception (missing key, unsupported algorithm, import error), the error is logged and
STORAGE_IMPLfalls back to the unwrapped plaintext backend .
⚠️ Initialization failure is silent at runtime — storage continues to work but writes are unencrypted. Monitor startup logs for
"Failed to initialize encrypted storage".
Configuration#
| Environment variable | Default | Description |
|---|---|---|
RAGFLOW_CRYPTO_ENABLED | false | Set true to activate encryption |
RAGFLOW_CRYPTO_KEY | (required) | Passphrase for key derivation |
RAGFLOW_CRYPTO_ALGORITHM | aes-256-cbc | One of: aes-256-cbc, aes-128-cbc, sm4-cbc |
STORAGE_IMPL | MINIO | Underlying backend (MINIO, AWS_S3, AZURE_SPN, AZURE_SAS, OSS, GCS, OPENDAL) |
CryptoUtil raises ValueError if RAGFLOW_CRYPTO_KEY is absent or if an unsupported algorithm name is provided , which triggers the fallback in init_settings.
Key Source Files#
| File | Purpose |
|---|---|
rag/utils/encrypted_storage.py | EncryptedStorageWrapper class and create_encrypted_storage() factory |
common/crypto_utils.py | CryptoUtil, BaseCrypto, AES256CBC, AES128CBC, SM4CBC |
common/settings.py | init_settings() — wiring, env-var resolution, fallback logic |
History#
- PR #11838 (merged Dec 2025) — initial implementation: added
crypto_utils.py,encrypted_storage.py, wired intosettings.py, addedcryptographydependency . - PR #13458 (Mar 2026) — proposed
StorageBaseProtocol to formalize the required backend interface, replacing ad-hochasattr()checks .