# How Open Notebook Secures API Keys in Database Credentials: Encryption & Security Measures

> Learn how Open Notebook secures API keys in database credentials with Fernet encryption. Protect your sensitive data at rest and prevent plaintext exposure. Discover robust security measures for your SurrealDB.

- Repository: [Luis Novo/open-notebook](https://github.com/lfnovo/open-notebook)
- Tags: security
- Published: 2026-07-04

---

**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`](https://github.com/lfnovo/open-notebook/blob/main/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.

```python

# 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`](https://github.com/lfnovo/open-notebook/blob/main/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.

```python
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`](https://github.com/lfnovo/open-notebook/blob/main/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.

```python
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.

```python

# 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.

```python
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_KEY` allows secure key rotation and Docker secrets support through `get_secret_from_env`.
- **Automatic field-level encryption** in [`open_notebook/domain/credential.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/domain/credential.py) encrypts 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`](https://github.com/lfnovo/open-notebook/blob/main/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`](https://github.com/lfnovo/open-notebook/blob/main/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.