How Open Notebook Manages and Encrypts API Keys in SurrealDB

Open Notebook encrypts API keys using Fernet symmetric encryption before storing them in SurrealDB, decrypting them transparently when credentials are retrieved using an environment-derived encryption key.

Open Notebook is an open-source application that securely stores AI provider credentials in SurrealDB. Understanding how the system manages and encrypts API keys in SurrealDB is crucial for maintaining security at rest. This article examines the implementation details found in the lfnovo/open-notebook repository, covering the encryption workflow from the domain model to the database layer.

The Encryption Architecture

Fernet Symmetric Encryption

At the core of the security layer is Fernet symmetric encryption, implemented in open_notebook/utils/encryption.py. The system uses a secret derived from the OPEN_NOTEBOOK_ENCRYPTION_KEY environment variable. This secret is hashed using SHA-256 and base64-url-encoded to create a valid Fernet key, ensuring that even simple passphrase strings can serve as secure encryption roots.

The Credential Domain Model

The Credential class in open_notebook/domain/credential.py serves as the Pydantic domain model. It wraps sensitive API keys in Pydantic SecretStr types, preventing accidental logging or serialization of plaintext values. When a credential is persisted, the model handles the encryption boundary automatically, ensuring that only encrypted tokens reach the database.

Storing Encrypted API Keys

When you save a credential, the api_key field undergoes encryption before reaching SurrealDB. In open_notebook/domain/credential.py (lines 34-42), the _prepare_save_data method extracts the secret value and passes it to open_notebook/utils/encryption.py, where the encrypt_value function generates a URL-safe Base64 Fernet token.

from open_notebook.domain.credential import Credential
from pydantic import SecretStr

cred = Credential(
    name="My OpenAI Key",
    provider="openai",
    modalities=["language", "embedding"],
    api_key=SecretStr("sk-my-super-secret-key"),
)

# `save()` encrypts the key and persists the record in SurrealDB.

await cred.save()

The generic repository layer in open_notebook/database/repository.py executes the SurrealQL commands, storing the encrypted token in the api_key column of the credential table alongside provider-specific configuration data.

Retrieving and Decrypting API Keys

Upon retrieval via Credential.get or Credential.get_all, the system automatically decrypts the stored token. The _from_db_row helper (lines 56-68 in open_notebook/domain/credential.py) calls decrypt_value from the encryption utility, which reverses the Fernet encryption process. The resulting plaintext is wrapped back into a SecretStr before being returned to the caller.

from open_notebook.domain.credential import Credential

# Load by SurrealDB record ID (e.g., "credential:abc123")

cred = await Credential.get("credential:abc123")

# `api_key` is a SecretStr containing the plaintext value.

plain_key = cred.api_key.get_secret_value()
print("Decrypted key:", plain_key)

Downstream code in open_notebook/ai/key_provider.py and open_notebook/ai/models.py accesses these decrypted credentials without needing to understand the underlying encryption layer.

Environment Configuration and Key Management

The encryption system implements lazy key loading to improve service resilience. Rather than reading OPEN_NOTEBOOK_ENCRYPTION_KEY at import time, the system loads the environment variable only when the first encryption or decryption call occurs (lines 90-102 in open_notebook/utils/encryption.py). This prevents the application from crashing during startup if the variable is temporarily missing.

If the encryption key changes or is incorrect, the system provides clear error boundaries. When decrypt_value encounters a value that appears to be a Fernet token but fails decryption (lines 88-95 in open_notebook/utils/encryption.py), it raises a descriptive error guiding the operator to verify the OPEN_NOTEBOOK_ENCRYPTION_KEY setting. This safety mechanism supports key rotation workflows by alerting operators when existing encrypted records require re-encryption.

Low-Level Encryption Utilities

For advanced use cases, the encryption module exposes direct utility functions:

from open_notebook.utils.encryption import encrypt_value, decrypt_value

cipher = encrypt_value("my-plain-api-key")
print("Encrypted token:", cipher)

plain = decrypt_value(cipher)
print("Decrypted back:", plain)

These functions handle the Fernet instance lifecycle and key derivation automatically, ensuring consistent encryption behavior across the application.

Summary

  • At-rest encryption: All API keys are stored as Fernet tokens in SurrealDB, protecting against database dump exposure.
  • Transparent decryption: The Credential domain model automatically decrypts keys when retrieving records, returning them as SecretStr objects.
  • Lazy key loading: The encryption key is read from OPEN_NOTEBOOK_ENCRYPTION_KEY only when needed, preventing startup failures.
  • Key rotation safety: Clear error messages indicate when decryption fails due to key mismatches, enabling operational key rotation.
  • Repository pattern: The generic database layer in open_notebook/database/repository.py handles SurrealQL execution while the domain model manages encryption logic.

Frequently Asked Questions

What encryption algorithm does Open Notebook use for API keys?

Open Notebook uses Fernet symmetric encryption from the Python cryptography library. The algorithm provides authenticated encryption, ensuring that data cannot be read or modified without the correct key. The encryption key is derived from the OPEN_NOTEBOOK_ENCRYPTION_KEY environment variable using SHA-256 hashing and base64-url-encoding.

How does Open Notebook handle missing or changed encryption keys?

The system implements lazy key loading, reading the environment variable only when the first encryption or decryption operation occurs. If the key is missing or incorrect at runtime, operations fail with clear error messages indicating that OPEN_NOTEBOOK_ENCRYPTION_KEY should be checked. This prevents silent failures and aids debugging during deployment or key rotation.

Can I rotate the encryption key for existing API credentials?

Key rotation requires re-encryption. If you change OPEN_NOTEBOOK_ENCRYPTION_KEY, existing encrypted tokens in the database will fail decryption with a specific error message. You must retrieve the credentials using the old key, then save them again with the new key to re-encrypt the data. The error handling in open_notebook/utils/encryption.py (lines 88-95) explicitly guides operators through this scenario.

Where does Open Notebook store the encryption key?

Open Notebook does not store the encryption key in the database. The key exists only as an environment variable (OPEN_NOTEBOOK_ENCRYPTION_KEY) on the application server. This design follows security best practices by separating the encryption key from the encrypted data, ensuring that database access alone does not compromise the API keys.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →