# How the Open-Notebook Credential Management System Stores API Keys with Fernet Encryption

> Learn how Open-Notebook stores API keys with Fernet encryption. It automatically encrypts and decrypts secrets using a derived key from your environment variable.

- Repository: [Luis Novo/open-notebook](https://github.com/lfnovo/open-notebook)
- Tags: how-to-guide
- Published: 2026-07-02

---

**Open-Notebook encrypts every AI-provider API key at rest using Fernet symmetric encryption (AES-128-CBC + HMAC-SHA256) by deriving a key from the `OPEN_NOTEBOOK_ENCRYPTION_KEY` environment variable, automatically encrypting secrets on save and decrypting them on load through the `Credential` domain model.**

The `lfnovo/open-notebook` repository implements a secure credential management system that ensures API keys never persist in plaintext in the SurrealDB database. By integrating Fernet symmetric encryption directly into the `Credential` model's persistence lifecycle in [`open_notebook/domain/credential.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/domain/credential.py), the system protects sensitive secrets at rest while allowing developers to work with standard Pydantic `SecretStr` objects throughout the application.

## Fernet Encryption Workflow

The credential management system operates through a transparent three-phase workflow that encrypts data before database insertion and decrypts it upon retrieval. This process ensures that raw API keys exist only in memory during active use.

### Key Derivation from Environment Variables

The encryption subsystem initializes by reading the `OPEN_NOTEBOOK_ENCRYPTION_KEY` environment variable (or its Docker Secret variant suffixed with `_FILE`). In [`open_notebook/utils/encryption.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/utils/encryption.py), the `_ensure_fernet_key()` function processes this value by hashing it with SHA-256 and encoding the result as a URL-safe Base64 string, which conforms to Fernet's required 32-byte key format.

*Source:* `_ensure_fernet_key` in [[`open_notebook/utils/encryption.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/utils/encryption.py)](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/utils/encryption.py#L5-L13)

### Encryption During Persistence

When a `Credential` instance is persisted via `save()`, the private `_prepare_save_data()` method intercepts the `api_key` field—which is stored internally as a Pydantic `SecretStr`. The method extracts the secret value and passes it to `encrypt_value()` in [`open_notebook/utils/encryption.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/utils/encryption.py), which returns a Fernet token (a URL-safe Base64 string). This ciphertext is what actually gets written to the `credential` table.

*Source:* `_prepare_save_data` handling of `api_key` in [[`open_notebook/domain/credential.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/domain/credential.py)](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/domain/credential.py#L34-L40) and [`encrypt_value()` in [`utils/encryption.py`](https://github.com/lfnovo/open-notebook/blob/main/utils/encryption.py)](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/utils/encryption.py#L28-L43)

### Decryption on Retrieval

Whenever credentials are loaded via `Credential.get()`, `Credential.get_all()`, or the internal `_from_db_row()` constructor, the system checks if the stored `api_key` value resembles a Fernet token. If so, it passes the ciphertext to `decrypt_value()`, which uses the same environment-derived key to restore the plaintext. The result is wrapped back into a `SecretStr` before being returned to the caller.

If the stored value is plaintext (legacy data), it is returned unchanged. If decryption fails due to a key mismatch, a clear `ValueError` is raised.

*Sources:* `Credential.get()` logic in [[`open_notebook/domain/credential.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/domain/credential.py) lines 58‑70](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/domain/credential.py#L58-L70), `Credential.get_all()` in [lines 73‑95](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/domain/credential.py#L73-L95), and [`decrypt_value()` in [`utils/encryption.py`](https://github.com/lfnovo/open-notebook/blob/main/utils/encryption.py)](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/utils/encryption.py#L66-L78)

## Practical Implementation Examples

The encryption layer operates transparently during credential creation and retrieval. The following examples demonstrate the developer-facing API and the underlying utility functions.

### Creating and Saving an Encrypted Credential

When you instantiate a `Credential` and call `save()`, the encryption happens automatically before the database write:

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

# Define a credential – the raw key is never stored plain-text

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

# Persist – _prepare_save_data() encrypts the key before the DB write

await cred.save()

```

The value stored in SurrealDB resembles a long Base64 string (e.g., `gAAAAABlY...`), not the raw API key.

### Retrieving and Decrypting Credentials

When loading credentials, decryption occurs transparently, returning a `SecretStr` containing the original value:

```python

# Load a credential by its SurrealDB record ID

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

# The api_key field is again a SecretStr containing the clear text

print(cred.api_key.get_secret_value())   # → "sk-my-super-secret-key"

```

### Low-Level Encryption Utilities

For custom utilities or data migrations, you can access the core encryption functions directly from the utilities module:

```python
from open_notebook.utils.encryption import encrypt_value, decrypt_value

plain = "sk-my-super-secret-key"
cipher = encrypt_value(plain)   # → Fernet token (Base64)

assert decrypt_value(cipher) == plain

```

### Handling Key Rotation Errors

If the `OPEN_NOTEBOOK_ENCRYPTION_KEY` environment variable changes after credentials are encrypted, subsequent retrieval attempts will fail with a descriptive error:

```python
try:
    cred = await Credential.get("credential:12345")
except ValueError as exc:
    # Raised when the stored token cannot be decrypted because the env key changed

    print("Decryption failed:", exc)

```

## Summary

The Open-Notebook credential management system implements a robust security architecture for API key storage:

- **Environment-based key derivation** utilizes SHA-256 hashing of the `OPEN_NOTEBOOK_ENCRYPTION_KEY` variable to produce Fernet-compatible encryption keys in `_ensure_fernet_key()`.
- **Automatic encryption** occurs in `_prepare_save_data()` within [`open_notebook/domain/credential.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/domain/credential.py), converting `SecretStr` values to Fernet tokens before database persistence via `encrypt_value()`.
- **Transparent decryption** happens during credential retrieval via `Credential.get()` and `Credential.get_all()`, ensuring application code always receives usable `SecretStr` objects while raw ciphertext never leaves the database layer.
- **Legacy compatibility** allows the system to handle unencrypted plaintext values gracefully during transitions to encrypted storage, while strict error handling prevents silent failures when encryption keys mismatch.

## Frequently Asked Questions

### What encryption algorithm does Fernet use?

Fernet implements **AES-128-CBC** for symmetric encryption combined with **HMAC-SHA256** for authentication. This construction ensures both confidentiality and integrity of the stored API keys, preventing unauthorized decryption or tampering even if the database is compromised.

### How is the encryption key configured?

The system reads the `OPEN_NOTEBOOK_ENCRYPTION_KEY` environment variable on startup. For Docker deployments, you can use the `_FILE` variant (e.g., `OPEN_NOTEBOOK_ENCRYPTION_KEY_FILE`) to load the key from a mounted secret file. The `_ensure_fernet_key()` function in [`open_notebook/utils/encryption.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/utils/encryption.py) hashes the input with SHA-256 to generate the final 32-byte Fernet key required by the cryptography library.

### What happens if the encryption key changes?

If the `OPEN_NOTEBOOK_ENCRYPTION_KEY` is modified after credentials have been encrypted, attempts to retrieve those credentials will raise a `ValueError`. The `decrypt_value()` function detects invalid tokens and fails loudly, preventing the application from using unrecoverable data. To recover, you must either restore the original key value or regenerate the credentials and re-encrypt them with the new key.

### How does the system handle legacy unencrypted data?

When loading credentials, the code checks whether the stored value resembles a valid Fernet token. If the value appears to be plaintext (legacy data that predates encryption implementation), it is returned unchanged wrapped in a `SecretStr`. This backward-compatible approach allows gradual migration to encrypted storage without breaking existing deployments.