How Open Notebook Secures API Keys in Database Credentials: Encryption & Security Measures
Open Notebook encrypts API keys using Fernet (AES-128-CBC + HMAC-SHA256) before persisting them to SurrealDB, ensuring that sensitive credentials remain confidential at rest and are never exposed as plaintext in the database.
The lfnovo/open-notebook repository implements field-level encryption for all API credentials, protecting against unauthorized database access while maintaining seamless usability for LLM integrations. This security architecture ensures that even if the underlying SurrealDB instance is compromised, attacker access is limited to encrypted tokens rather than raw API keys. The implementation spans utility functions for cryptographic operations and domain model logic that automatically handles encryption on writes and decryption on reads.
The Encryption Architecture
Open Notebook employs Fernet symmetric encryption from the Python cryptography library, which combines AES-128 in CBC mode with HMAC-SHA256 for authenticated encryption. This approach provides both confidentiality and integrity verification for stored API keys.
Encryption Key Management
The application derives its Fernet key from the OPEN_NOTEBOOK_ENCRYPTION_KEY environment variable, supporting both direct string values and Docker secrets via file references. In open_notebook/utils/encryption.py, the get_secret_from_env helper reads the configuration, while _get_or_create_encryption_key validates the key material and raises a clear error if the environment variable is missing.
# Key derivation happens lazily to allow startup without immediate configuration
from open_notebook.utils.encryption import _get_or_create_encryption_key
# Raises ValueError if OPEN_NOTEBOOK_ENCRYPTION_KEY is not set
fernet_key = _get_or_create_encryption_key()
The derived key is cached after first access, preventing repeated file system or environment lookups while ensuring the application can start even if the encryption key is configured asynchronously.
Field-Level Encryption Process
When encrypting values, the encrypt_value function in open_notebook/utils/encryption.py instantiates a Fernet cipher using get_fernet() and returns a base64-encoded token. This token is what actually gets stored in the database's api_key field, replacing the sensitive plaintext.
from open_notebook.utils.encryption import encrypt_value
# Returns a Fernet token (base64-encoded ciphertext)
encrypted_token = encrypt_value("sk-secret-api-key")
Credential Model Integration
The Credential domain object in open_notebook/domain/credential.py orchestrates the encryption lifecycle, automatically securing API keys during persistence operations and transparently decrypting them during retrieval.
Automatic Encryption on Save
The Credential class overrides _prepare_save_data to intercept the api_key field before database writes. This method calls encrypt_value on the plaintext key, ensuring that SurrealDB only receives the encrypted token.
from open_notebook.domain.credential import Credential
from pydantic import SecretStr
# Create a credential with a sensitive API key
cred = Credential(
name="Production OpenAI",
provider="openai",
modalities=["language", "embedding"],
api_key=SecretStr("sk-production-key")
)
# Encryption happens automatically here before DB insertion
await cred.save()
Decryption and Secret Handling
During read operations (Credential.get, Credential.get_all, and _from_db_row), the stored token is decrypted using decrypt_value and re-wrapped in a Pydantic SecretStr object. This prevents accidental exposure of the key in logs, tracebacks, or API responses.
# Retrieve and use the decrypted credential
cred = await Credential.get("credential:123")
api_key = cred.api_key.get_secret_value() # Decrypts and returns plaintext
If decryption fails—typically due to a changed OPEN_NOTEBOOK_ENCRYPTION_KEY—the model attaches a decryption_error attribute and returns a placeholder value, preventing application crashes while alerting operators to the key mismatch.
Graceful Degradation and Error Handling
The decrypt_value function implements intelligent fallback logic to handle legacy data and configuration changes. First, it attempts standard Fernet decryption. If the token appears to be a valid Fernet ciphertext but decryption fails, it raises a specific ValueError indicating that the encryption key is incorrect.
For backwards compatibility, if the stored value is plaintext (legacy data from before encryption was implemented), the function returns it unchanged rather than failing. This ensures that existing credentials continue to function while new ones automatically receive encryption protection.
from open_notebook.utils.encryption import decrypt_value
# Handles both encrypted tokens and legacy plaintext
try:
api_key = decrypt_value(stored_token)
except ValueError as e:
# Raised when encryption key has changed or token is corrupted
print(f"Decryption failed: {e}")
Summary
- Fernet encryption (AES-128-CBC + HMAC-SHA256) protects all API keys before they reach the database, ensuring confidentiality at rest.
- Environment-based key management via
OPEN_NOTEBOOK_ENCRYPTION_KEYallows secure key rotation and Docker secrets support throughget_secret_from_env. - Automatic field-level encryption in
open_notebook/domain/credential.pyencrypts on save and decrypts on load without requiring manual intervention. - SecretStr wrapping prevents accidental exposure of decrypted keys in application logs or stack traces.
- Graceful fallback handling supports legacy plaintext credentials while providing clear error messages when encryption keys mismatch.
Frequently Asked Questions
How does Open Notebook handle API key rotation or encryption key changes?
When the OPEN_NOTEBOOK_ENCRYPTION_KEY environment variable changes, existing encrypted tokens cannot be decrypted with the new key. The decrypt_value function in open_notebook/utils/encryption.py raises a clear ValueError explaining that the encryption key is wrong, and the Credential model attaches a decryption_error flag to the affected record. Operators must re-save credentials with the new key to update the encrypted storage.
What happens if the OPEN_NOTEBOOK_ENCRYPTION_KEY is not configured?
The application uses lazy initialization via _get_or_create_encryption_key to allow startup without immediate encryption configuration. However, attempting to save a credential with an API key will trigger encryption, which requires the key. If the key is missing at that point, the function raises a descriptive error indicating that the encryption key must be set.
Does Open Notebook support BYOK (Bring Your Own Key) or external key management services?
Currently, the implementation relies on the OPEN_NOTEBOOK_ENCRYPTION_KEY environment variable or Docker secrets file references via get_secret_from_env. While the architecture supports any 32-byte Fernet key provided through these mechanisms, native integration with external KMS providers like AWS KMS or HashiCorp Vault would require extending the get_fernet function in open_notebook/utils/encryption.py to support key retrieval from those services.
Are API keys ever logged or exposed in the UI with this encryption?
No. The Credential domain model wraps decrypted values in Pydantic SecretStr objects, which mask the value as ******** when converted to strings or serialized. The encryption utilities ensure that only base64-encoded Fernet tokens appear in database queries, logs, or network traffic between the application and SurrealDB.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →