# Credential Encryption Using Fernet in Open‑Notebook: How API Keys Are Secured

> Secure your API keys with credential encryption using Fernet in Open-Notebook. Learn how the cryptography library secures sensitive data with an environment variable key.

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

---

**Open‑Notebook uses Fernet symmetric encryption from the cryptography library to encrypt API keys before storing them in the database, deriving the encryption key from the `OPEN_NOTEBOOK_ENCRYPTION_KEY` environment variable.**

The **lfnovo/open-notebook** repository implements a robust security layer for sensitive provider credentials. Credential encryption using Fernet ensures that API keys never persist in plaintext, protecting secrets at rest while maintaining transparent access patterns for the application layer.

## Where the Encryption Key Comes From

Open‑Notebook reads the master secret from the **`OPEN_NOTEBOOK_ENCRYPTION_KEY`** environment variable. In containerized deployments, you can alternatively mount a Docker secret to a file path specified by **`OPEN_NOTEBOOK_ENCRYPTION_KEY_FILE`**.

The resolution logic lives in [`open_notebook/utils/encryption.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/utils/encryption.py) at line 62 inside the `_get_or_create_encryption_key` function. If neither variable is present, the module raises a clear `ValueError`, forcing operators to configure a secret before any encrypted data can be stored.

## How Fernet Keys Are Derived

Fernet expects a 32‑byte URL‑safe base‑64 encoded key, but operators can supply any arbitrary passphrase. The `_ensure_fernet_key` function at line 104 in [`open_notebook/utils/encryption.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/utils/encryption.py) bridges this gap by:

1. Hashing the input string using **SHA‑256**.
2. Base‑64 encoding the digest to produce a deterministic Fernet key.

This approach guarantees that the same passphrase always generates the same encryption key, simplifying backups and disaster recovery without storing the raw Fernet key itself.

## The Encryption and Decryption API

The encryption module exposes three core utilities that handle all cryptographic operations:

- **`encrypt_value(value: str) → str`** – Implemented at line 28 in [`open_notebook/utils/encryption.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/utils/encryption.py), this function encrypts a plaintext string and returns a URL‑safe base‑64 token.
- **`decrypt_value(value: str) → str`** – Starting at line 66, this function attempts Fernet decryption. If the token is valid, it returns the plaintext; if the token appears to be a Fernet token but decryption fails, it raises a `ValueError`.
- **`looks_like_fernet_token(s: str) → bool`** – Located at line 45, this helper inspects token length and structure to distinguish encrypted blobs from legacy plaintext.

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

# Encrypting a secret

encrypted = encrypt_value("sk-secret-key")

# Returns a Fernet token like: 'gAAAAAB...'

# Decrypting it back

decrypted = decrypt_value(encrypted)

# Returns: 'sk-secret-key'

```

## Integration with the Credential Model

The `Credential` domain model in [`open_notebook/domain/credential.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/domain/credential.py) treats API keys as `Optional[SecretStr]` (line 61). This Pydantic type ensures that secrets are never accidentally logged or serialized into JSON responses.

When a credential is **saved**, the repository layer calls `encrypt_value` via the `ObjectModel._to_db_row` helper before persisting the string to the database. When retrieving credentials, the `Credential.get` method (line 55) and `Credential.get_all` method (line 71) invoke `decrypt_value` on the stored string, then wrap the result back in a `SecretStr`.

This flow is transparent to callers, but any decryption failure is captured in the `decryption_error` field for UI display rather than crashing the request.

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

async def create_and_retrieve():
    # Create with plaintext; it gets encrypted before storage

    cred = Credential(
        name="Production",
        provider="openai",
        modalities=["language"],
        api_key=SecretStr("sk-my-secret-key"),
    )
    await cred.save()  # encrypt_value runs under the hood

    
    # Retrieve and decrypt automatically

    fetched = await Credential.get(cred.id)
    api_key = fetched.api_key.get_secret_value()
    print(f"Decrypted API key: {api_key[:4]}...")

```

## Handling Legacy Data and Key Rotation

Open‑Notebook includes safety checks for operational continuity.

**Backward compatibility** is handled by `decrypt_value` at line 66: if `looks_like_fernet_token` returns `False`, the function assumes the stored value is legacy plaintext and returns it unchanged. This allows the system to read credentials that were never encrypted without requiring a migration script.

**Key rotation errors** are captured at line 87 in [`open_notebook/utils/encryption.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/utils/encryption.py). When `InvalidToken` is raised (indicating the encryption key changed), the `Credential.get_all` method (lines 86‑95) records a `decryption_error` message instead of propagating the exception. Administrators can audit these errors without breaking the UI.

```python
import asyncio
from open_notebook.domain.credential import Credential

async def audit_credentials():
    creds = await Credential.get_all()
    for c in creds:
        if c.decryption_error:
            print(f"{c.name}: DECRYPTION FAILED – {c.decryption_error}")
        else:
            print(f"{c.name}: OK")

```

## Summary

- **Key derivation**: `_ensure_fernet_key` in [`open_notebook/utils/encryption.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/utils/encryption.py) creates deterministic Fernet keys from the `OPEN_NOTEBOOK_ENCRYPTION_KEY` environment variable using SHA‑256 hashing.
- **Encryption**: `encrypt_value` encrypts API keys before database storage, while `decrypt_value` handles retrieval and automatically manages legacy plaintext.
- **Model integration**: The `Credential` class in [`open_notebook/domain/credential.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/domain/credential.py) uses Pydantic `SecretStr` to prevent accidental exposure and invokes encryption utilities during save/load operations.
- **Error handling**: Decryption failures are surfaced in the `decryption_error` field, allowing graceful degradation when keys rotate or tokens corrupt.

## Frequently Asked Questions

### How do I configure the encryption key for Open‑Notebook?

Set the **`OPEN_NOTEBOOK_ENCRYPTION_KEY`** environment variable to a strong passphrase before starting the application. For Docker deployments, you can use **`OPEN_NOTEBOOK_ENCRYPTION_KEY_FILE`** to point to a file containing the secret. If neither is configured, the application raises a `ValueError` on startup to prevent unencrypted storage.

### What happens if I change the encryption key after credentials are stored?

If you rotate the `OPEN_NOTEBOOK_ENCRYPTION_KEY`, existing encrypted credentials will fail to decrypt. The `decrypt_value` function raises `InvalidToken` when this occurs, and the `Credential.get_all` method captures this in the `decryption_error` field. You must re‑encrypt credentials with the new key or restore the original key to recover access.

### Can Open‑Notebook read unencrypted credentials from older versions?

Yes. The `decrypt_value` function in [`open_notebook/utils/encryption.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/utils/encryption.py) includes a `looks_like_fernet_token` check. If a stored credential does not match the Fernet token format, the function returns the raw value unchanged, allowing seamless migration from legacy plaintext storage to encrypted storage.

### Why does the code use SecretStr instead of plain strings?

The `Credential` model uses Pydantic's **`SecretStr`** type (defined in [`open_notebook/domain/credential.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/domain/credential.py)) to prevent accidental logging or JSON serialization of API keys. The actual secret is only exposed when you explicitly call `.get_secret_value()`, adding a safety layer against debug output and log leaks.