Credential Vault
How Open Connector encrypts and stores OAuth tokens and API keys.
Open Connector never stores credentials in plaintext. Every OAuth access token, refresh token, and API key is encrypted before being written to the database and decrypted only at the moment it is injected into an outbound proxy request. Your infrastructure is the only place where plaintext credentials exist — only for the duration of a single request.
Encryption scheme
Open Connector uses AES-256-GCM for symmetric authenticated encryption. The 32-byte encryption key is derived from your master secret with HKDF-SHA-256, using a fixed domain-separation label so the master secret is never used as the cipher key directly.
Key derivation
The master secret is the CONNECTOR_ENCRYPTION_KEY environment variable (minimum 32 characters). At startup, HKDF-SHA-256 derives the 32-byte AES key from it:
master_secret = CONNECTOR_ENCRYPTION_KEY // ≥ 32 chars
key = HKDF-SHA-256(master_secret, info="open-connector-vault-v1", length=32)The derived key lives only in the server process for the lifetime of the run. The master secret is never used as the cipher key directly, and the derived key is never written anywhere.
Encryption
Each credential blob is encrypted with AES-256-GCM. A fresh random 12-byte IV is generated per encryption, and the stored blob bundles the IV, the GCM authentication tag, and the ciphertext (each base64, dot-separated):
iv = random_12_bytes
ciphertext, auth_tag = AES_256_GCM_encrypt(key, iv, plaintext)
stored = base64(iv) + "." + base64(auth_tag) + "." + base64(ciphertext)The IV is random per encryption, so identical plaintext values produce different ciphertexts. The blob is self-contained — the IV and tag travel with the ciphertext, so no separate IV table is needed. The implementation is in packages/connectors/src/vault.ts.
Authentication
AES-GCM provides authenticated encryption: the auth tag verifies both that the ciphertext has not been tampered with and that the correct key was used. Any bit flip in the stored credential will cause decryption to fail with an authentication error, not silently return garbage.
What is stored encrypted
A connection's secrets are serialized to JSON and stored as a single AES-256-GCM blob in the connection.credentials column. The exact fields depend on the connector's auth mode:
| Data | Auth mode | Stored as |
|---|---|---|
| OAuth access + refresh tokens | OAUTH2 | encrypted JSON in connection.credentials |
| API keys | API_KEY | encrypted JSON in connection.credentials |
| Basic auth username + password | BASIC | encrypted JSON in connection.credentials |
| OAuth client id + secret (auth configs) | — | encrypted JSON in auth_config.client_credentials |
Everything in those columns is AES-256-GCM encrypted; non-sensitive metadata (status, provider slug, scopes, expiry) is stored in plaintext alongside.
What never leaves your infrastructure
- Plaintext tokens and secrets never appear in logs, API responses, or error messages. Open Connector redacts these fields before any structured log event is emitted.
- The derived encryption key exists only in the server process and is not persisted anywhere.
- The master secret (
CONNECTOR_ENCRYPTION_KEY) exists only in your server's environment and is never transmitted over the network. - Client secrets for OAuth apps are write-only: accepted on create/update, encrypted immediately, and never returned by any read endpoint. The
GET /auth_configs/{id}response shape carries noclient_idorclient_secretfield at all — the internal auth-config view is projected without credential fields.
If your CONNECTOR_ENCRYPTION_KEY is lost or rotated without re-encrypting existing credentials, all stored credentials become permanently undecryptable. Treat this key with the same care as a database encryption master key — store it in a secrets manager and back it up securely.
The CONNECTOR_ENCRYPTION_KEY environment variable
Set this in your server environment before the first run:
# Generate a secure random key (32+ bytes)
openssl rand -base64 32
# Example output: 4k8mN2p9qR7sT1uV3wX5yZ6aB0cD8eF2gH4iJ6kL8=Add it to your environment:
# .env (never commit this file)
CONNECTOR_ENCRYPTION_KEY=4k8mN2p9qR7sT1uV3wX5yZ6aB0cD8eF2gH4iJ6kL8=The key must be at least 32 characters. The server will refuse to start if this variable is missing or shorter than the minimum length.
Key rotation
Rotating CONNECTOR_ENCRYPTION_KEY requires re-encrypting every stored credential with the new key — replacing the variable on its own makes all existing credentials undecryptable.
There is no built-in key-rotation command today. Until rotation tooling ships, treat the initial CONNECTOR_ENCRYPTION_KEY as durable infrastructure: back it up securely and avoid changing it. A manual rotation means reading each credential, decrypting with the old key, re-encrypting with the new key, and writing it back — the encryption primitives are in packages/connectors/src/vault.ts.
Database security
The encrypted blobs stored in Postgres are opaque to anyone with database access but not the server's encryption key. As an additional defense-in-depth measure:
- Enable Postgres TLS in transit between your server and database.
- Use Postgres row-level security or a dedicated database user with access only to the tables Open Connector needs.
- Enable database-level encryption at rest (most managed Postgres providers do this by default).
These measures ensure that even a full database dump is useless without CONNECTOR_ENCRYPTION_KEY.