Open Connector
Security

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:

DataAuth modeStored as
OAuth access + refresh tokensOAUTH2encrypted JSON in connection.credentials
API keysAPI_KEYencrypted JSON in connection.credentials
Basic auth username + passwordBASICencrypted 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 no client_id or client_secret field 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.

On this page