How the Credential System in Open Notebook Manages AI Provider API Keys
Open Notebook's credential system encrypts AI provider API keys in SurrealDB using a project-wide secret, exposes them through SSRF-protected FastAPI endpoints that never return raw keys, and supports migration, validation, and connection testing via the service layer.
The credential system in Open Notebook unifies AI provider authentication under a single Credential domain model backed by SurrealDB. Every API key is encrypted at rest with OPEN_NOTEBOOK_ENCRYPTION_KEY, and the HTTP layer enforces encryption-key presence while returning only a has_api_key boolean to callers. All implementation logic is split across three primary files: open_notebook/domain/credential.py for data modeling and encryption, api/credentials_service.py for business rules, and api/routers/credentials.py for the FastAPI interface.
Credential System in Open Notebook: Domain Model and Encryption
In open_notebook/domain/credential.py, the Credential class extends ObjectModel and sets table_name = "credential". It declares fields including name, provider, modalities, and api_key, where api_key is typed as Optional[SecretStr] and included in nullable_fields.
When save() is invoked, the internal _prepare_save_data() method encrypts api_key via encrypt_value() defined in open_notebook/utils/encryption.py. On retrieval, Credential.get() and _from_db_row() decrypt the stored blob with decrypt_value() and re-wrap the plaintext in a SecretStr. This guarantees that SurrealDB never holds an unencrypted key.
For downstream AI consumption, to_esperanto_config() returns a plain dictionary containing "api_key", "base_url", and optional "endpoint" keys that Esperanto’s AIFactory consumes directly.
cred = Credential(..., api_key=SecretStr("sk-..."))
config = cred.to_esperanto_config()
# → {"api_key": "sk-...", "base_url": "...", "endpoint": "..."}
Relationship tracking is handled by get_linked_models(), which queries the model table for rows where credential = $cred_id. This lets the UI surface how many models depend on a given credential without exposing its value.
Credential System in Open Notebook: Service Layer and Validation
The api/credentials_service.py module orchestrates URL validation, connectivity testing, model discovery, and migration.
Provider Status and Environment Checks
get_provider_status() reports whether each provider is configured through database credentials or fallback environment variables, and confirms that OPEN_NOTEBOOK_ENCRYPTION_KEY is exported. check_env_configured(provider) inspects PROVIDER_ENV_CONFIG—a mapping of required environment variables per provider—to decide whether automatic migration is possible.
URL Validation and SSRF Protection
validate_url(url, provider) rejects schemes other than http or https and blocks link-local addresses in the 169.254.x.x range. This prevents server-side request forgery when users supply custom endpoints for providers such as Ollama or Azure.
Encryption-Key Enforcement
Before any write operation, require_encryption_key() verifies that OPEN_NOTEBOOK_ENCRYPTION_KEY exists. If the variable is missing, the function raises a ValueError, which the router converts into an HTTP 400 response via _handle_value_error. This stops accidental plain-text storage.
Connection Testing
test_credential(credential_id) assembles an Esperanto configuration from the stored record and dispatches to a provider-specific tester. For hosted providers it instantiates a lightweight model and performs a simple invoke; for Ollama and Azure it delegates to _test_ollama_connection and _test_azure_connection in open_notebook/ai/connection_tester.py. The result is a JSON object with provider, success, and message fields.
Model Discovery and Registration
discover_with_config(provider, config)contacts the provider’s/modelsendpoint or returns a static list for providers that do not expose a discovery API.register_models(credential_id, models_data)createsModelrecords in SurrealDB, links them to the credential, and skips duplicates using a case-insensitive key set. It leveragesclassify_model_typefromopen_notebook/ai/model_discovery.pyto tag model roles.
Migration Utilities
migrate_from_provider_config()reads the legacy singletonProviderConfigobject, converts each entry into an encryptedCredential, and re-links any previously unassigned models.migrate_from_env()iterates overPROVIDER_ENV_CONFIG, creates aCredentialper detected provider, encrypts it, and links orphaned models.
Both migrations require OPEN_NOTEBOOK_ENCRYPTION_KEY and log detailed progress to the application logs.
Credential System in Open Notebook: FastAPI Router and Safe Responses
The api/routers/credentials.py router exposes the credential system over HTTP while guaranteeing that raw API keys are never leaked. Request and response shapes are defined by Pydantic schemas in api/models.py, such as CreateCredentialRequest and CredentialResponse.
Credential CRUD Endpoints
- GET
/credentials—list_credentialsreturns all credentials with linked model counts. An optionalproviderfilter is supported. - POST
/credentials—create_credentialvalidates URLs viavalidate_url(), enforces the encryption key, and stores a new encrypted credential. - GET
/credentials/{id}—get_credentialreturns metadata; the response fieldhas_api_keyreveals presence without exposing the value. - PUT
/credentials/{id}—update_credentialallows partial updates and re-encrypts the key on save. - DELETE
/credentials/{id}—delete_credentialcascades deletion to linked models or migrates them to another credential.
Testing, Discovery, and Migration Endpoints
Additional POST routes support operational workflows:
/credentials/{id}/test— Calls the service-layer tester and returns connectivity results./credentials/{id}/discover— Returns discoverable models for the credential’s provider./credentials/{id}/register-models— Persists selected models and attaches them to the credential./migrate-from-env— Triggersmigrate_from_env()./migrate-from-provider-config— Triggersmigrate_from_provider_config().- GET
/status— Returns global configuration status including encryption-key presence and per-provider readiness. - GET
/env-status— Shows which providers have environment variables set.
All routes catch service-layer ValueErrors and surface them as HTTP 400 responses.
Practical Usage Examples for the Credential System in Open Notebook
Creating a Credential via the API
import httpx
BASE_URL = "http://localhost:5055"
payload = {
"name": "My OpenAI Key",
"provider": "openai",
"modalities": ["language", "embedding"],
"api_key": "sk-xxxxxxxxxxxxxxxxxxxx",
}
resp = httpx.post(f"{BASE_URL}/credentials", json=payload)
print(resp.json())
# → {"id":"c1...", "name":"My OpenAI Key", "provider":"openai", "has_api_key":true, ...}
The endpoint handler in api/routers/credentials.py calls the service layer, enforces encryption, and intentionally omits the key from the serialized CredentialResponse.
Testing a Stored Credential
import httpx
cred_id = "c1..."
resp = httpx.post(f"{BASE_URL}/credentials/{cred_id}/test")
print(resp.json())
# Example output:
# {"provider":"openai","success":true,"message":"Connection successful"}
Under the hood, the request flows from test_credential in the router to svc_test_credential and finally to test_credential in api/credentials_service.py, which uses Esperanto to verify connectivity.
Discovering and Registering Models
Discovery:
import httpx
resp = httpx.post(f"{BASE_URL}/credentials/{cred_id}/discover")
data = resp.json()
for m in data["discovered"]:
print(m["name"], m.get("description", ""))
Registration:
import httpx
models_to_register = [
{"name": "gpt-4o-mini", "provider": "openai", "model_type": "language"},
{"name": "text-embedding-3-large", "provider": "openai", "model_type": "embedding"},
]
payload = {"models": models_to_register}
resp = httpx.post(f"{BASE_URL}/credentials/{cred_id}/register-models", json=payload)
print(resp.json())
# → {"created":2, "existing":0}
Migrating from Environment Variables
# Requires OPEN_NOTEBOOK_ENCRYPTION_KEY to be set
curl -X POST http://localhost:5055/credentials/migrate-from-env
This invokes migrate_from_env in api/credentials_service.py, creating encrypted Credential records from the provider variables defined in PROVIDER_ENV_CONFIG.
Summary
- The credential system in Open Notebook persists AI provider secrets as encrypted
Credentialrecords in SurrealDB, usingOPEN_NOTEBOOK_ENCRYPTION_KEYfor at-rest protection. open_notebook/domain/credential.pyhandles domain modeling, encryption viaencrypt_value()anddecrypt_value(), and Esperanto-compatible config generation withto_esperanto_config().api/credentials_service.pyimplements business logic including SSRF-resistant URL validation, connection testing, model discovery and registration, and migrations from legacy or environment-based configs.api/routers/credentials.pyexposes CRUD, test, and migration endpoints while guaranteeing raw API keys are never returned to callers; only ahas_api_keyboolean is exposed.
Frequently Asked Questions
How are API keys encrypted in Open Notebook?
API keys are encrypted at rest using the OPEN_NOTEBOOK_ENCRYPTION_KEY environment variable. When a Credential is saved in open_notebook/domain/credential.py, _prepare_save_data() calls encrypt_value() from open_notebook/utils/encryption.py to transform the SecretStr payload before it reaches SurrealDB. The key is only decrypted back into a SecretStr during retrieval inside Credential.get() or _from_db_row().
Can I migrate existing provider configurations to the credential system?
Yes. Open Notebook provides two migration paths in api/credentials_service.py: migrate_from_provider_config() converts legacy ProviderConfig records into encrypted Credential objects, and migrate_from_env() reads provider-specific environment variables listed in PROVIDER_ENV_CONFIG and creates credentials from them. Both migrations re-link orphaned models and require the encryption key to be set.
Why does the credential system block some custom endpoint URLs?
The router applies validate_url(url, provider) from the service layer, which rejects non-HTTP(S) schemes and blocks link-local addresses such as 169.254.x.x. This SSRF protection prevents the server from being coerced into making unauthorized requests to internal infrastructure when users configure custom endpoints for providers like Ollama or Azure.
How do I verify that a stored credential can connect to its provider?
Send a POST request to /credentials/{id}/test. The service layer’s test_credential() function builds an Esperanto config from the stored record, dispatches to a provider-specific tester such as _test_ollama_connection or _test_azure_connection from open_notebook/ai/connection_tester.py, and returns a JSON object containing provider, success, and message fields. If the credential is invalid or the provider unreachable, the response explains the failure.
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 →