# How Logto Handles Secrets Management: A Deep Dive into the Secret Vault Architecture

> Discover how Logto handles secrets management with its Secret Vault architecture leveraging dual encryption layers AES-256-GCM and PostgreSQL for secure, efficient storage.

- Repository: [Logto/logto](https://github.com/logto-io/logto)
- Tags: deep-dive
- Published: 2026-07-06

---

**Logto implements a two-layer encryption system using a runtime Key-Encryption-Key (KEK) and per-secret Data-Encryption-Keys (DEK) with AES-256-GCM, storing encrypted payloads in a PostgreSQL-backed Secret Vault with automatic relational cleanup.**

Logto is an open-source identity management platform that securely stores sensitive credentials like OAuth tokens and SSO configurations. The platform's **Secret Vault** architecture ensures that all sensitive data is encrypted at rest using industry-standard algorithms while maintaining strict relational integrity to prevent orphaned data.

## Architectural Overview of the Secret Vault

The Secret Vault follows an "encrypt-at-rest, decrypt-on-demand" model designed to protect credentials that must persist beyond a single user session. This includes access tokens, refresh tokens, and enterprise SSO configurations.

### The Two-Layer Encryption Model (KEK + DEK)

Logto employs a hierarchical key structure to balance security with operational flexibility:

- **Key-Encryption-Key (KEK)** – A 256-bit master key supplied at runtime via the `SECRET_VAULT_KEK` environment variable. This key is accessed through [`EnvSet.values.secretVaultKek`](https://github.com/logto-io/logto/blob/master/packages/shared/src/node/env/GlobalValues.ts) and remains in memory only during application execution.
- **Data-Encryption-Key (DEK)** – A unique 256-bit key generated for every secret stored in the vault. The DEK encrypts the actual payload, while the KEK encrypts the DEK itself.

This separation ensures that compromising a single secret's DEK does not expose other secrets, and the KEK can be rotated without re-encrypting all stored data immediately.

### Encryption Algorithm and Implementation

All cryptographic operations use **AES-256-GCM** (Galois/Counter Mode) with authenticated encryption. The implementation in [[`packages/core/src/utils/secret-encryption.ts`](https://github.com/logto-io/logto/blob/main/packages/core/src/utils/secret-encryption.ts)](https://github.com/logto-io/logto/blob/master/packages/core/src/utils/secret-encryption.ts) provides four core functions:

- `encryptSecret(plaintext: string)` – Generates a DEK, encrypts the plaintext, then encrypts the DEK with the KEK. Returns a payload containing the initialization vector (`iv`), authentication tag (`authTag`), ciphertext, and encrypted DEK.
- `decryptSecret(encryptedPayload)` – Decrypts the DEK using the KEK, then decrypts the ciphertext using the recovered DEK.
- `encryptTokens(tokenSet)` – A wrapper that serializes a token set object to JSON before encryption.
- `decryptTokens(encryptedPayload)` – Decrypts and parses the JSON back into a typed `TokenSet` object.

If the `SECRET_VAULT_KEK` environment variable is missing, both `encryptSecret` and `decryptSecret` throw explicit errors to prevent accidental plaintext storage or decryption failures.

## Database Schema and Storage

Encrypted secrets persist in PostgreSQL with strict type safety and relational constraints.

### The Secrets Table Structure

The [`secrets`](https://github.com/logto-io/logto/blob/master/packages/schemas/src/db-entries/secret.js) table stores the encrypted payload alongside metadata. The TypeScript definitions in [[`packages/schemas/src/types/secrets.ts`](https://github.com/logto-io/logto/blob/main/packages/schemas/src/types/secrets.ts)](https://github.com/logto-io/logto/blob/master/packages/schemas/src/types/secrets.ts) enforce the schema structure:

- `id` – UUID primary key
- `tenantId` – Multi-tenancy isolation
- `encryptedPayload` – Contains `iv`, `authTag`, `ciphertext`, and `encryptedDek` as a JSON object
- `createdAt` / `updatedAt` – Audit timestamps

The actual database definition in [`packages/schemas/src/db-entries/secret.js`](https://github.com/logto-io/logto/blob/main/packages/schemas/src/db-entries/secret.js) maps these fields to PostgreSQL columns with appropriate constraints.

### Relational Cleanup with Foreign Key Triggers

To prevent orphaned secrets when connectors or identities are deleted, Logto maintains two junction tables created by the migration [[`1.30.0-1751255436-split-secret-connector-relations-table.ts`](https://github.com/logto-io/logto/blob/main/1.30.0-1751255436-split-secret-connector-relations-table.ts)](https://github.com/logto-io/logto/blob/master/packages/schemas/alterations/1.30.0-1751255436-split-secret-connector-relations-table.ts):

- **`secret_social_connector_relations`** – Links secrets to social connectors and user-social identities
- **`secret_enterprise_sso_connector_relations`** – Links secrets to enterprise SSO configurations

These tables include database triggers that automatically delete the associated secret record when the parent connector or identity is removed, ensuring referential integrity without application-level cleanup logic.

## Core Implementation Files

### Encryption Utilities

The [[`packages/core/src/utils/secret-encryption.ts`](https://github.com/logto-io/logto/blob/main/packages/core/src/utils/secret-encryption.ts)](https://github.com/logto-io/logto/blob/master/packages/core/src/utils/secret-encryption.ts) file contains the cryptographic engine. When `encryptTokens` is called, it:

1. Serializes the token set to JSON
2. Generates a random 256-bit DEK using `crypto.randomBytes(32)`
3. Encrypts the JSON with AES-256-GCM, producing an IV, auth tag, and ciphertext
4. Encrypts the DEK with the KEK using the same algorithm
5. Returns a structured object ready for database insertion

### Environment Configuration

The KEK is loaded from the environment in [[`packages/shared/src/node/env/GlobalValues.ts`](https://github.com/logto-io/logto/blob/main/packages/shared/src/node/env/GlobalValues.ts)](https://github.com/logto-io/logto/blob/master/packages/shared/src/node/env/GlobalValues.ts). The `EnvSet` class validates that `SECRET_VAULT_KEK` is present at startup, throwing a fatal error if the variable is undefined or empty.

### Database Access Layer

The [`SecretQueries`](https://github.com/logto-io/logto/blob/master/packages/core/src/queries/secret.ts) class in [`packages/core/src/queries/secret.ts`](https://github.com/logto-io/logto/blob/main/packages/core/src/queries/secret.ts) provides type-safe database operations:

- `insert(secret)` – Persists a new encrypted secret with tenant isolation
- `findById(id)` – Retrieves a secret by UUID, respecting Row-Level Security (RLS) policies
- `delete(id)` – Removes a secret and cascades through relation tables

## Practical Usage in Connectors

Social and enterprise connectors use the Secret Vault to store OAuth tokens securely.

### Social and Enterprise Connector Integration

When a connector completes an OAuth flow, the route handler in [[`packages/core/src/routes/connector/index.ts`](https://github.com/logto-io/logto/blob/main/packages/core/src/routes/connector/index.ts)](https://github.com/logto-io/logto/blob/master/packages/core/src/routes/connector/index.ts) or [[`packages/core/src/routes/sso-connector/index.ts`](https://github.com/logto-io/logto/blob/main/packages/core/src/routes/sso-connector/index.ts)](https://github.com/logto-io/logto/blob/master/packages/core/src/routes/sso-connector/index.ts) encrypts the token set before persistence.

```typescript
import { encryptTokens } from '@logto/core/src/utils/secret-encryption.js';
import { SecretQueries } from '@logto/core/src/queries/secret.js';

// Encrypt a token set before database storage
const tokenSet = {
  accessToken: 'eyJhbGciOiJIUzI1NiIs...',
  refreshToken: 'dGhpcyBpcyBhIHJlZnJlc2g...',
  scope: 'openid profile email',
  expiresAt: Date.now() + 3600000,
};

const encrypted = encryptTokens(tokenSet);

await new SecretQueries().insert({
  tenantId: 'tenant_123',
  userId: 'user_456',
  secret: encrypted,
});

```

When the connector needs to call an external API, it retrieves and decrypts the tokens:

```typescript
import { decryptTokens } from '@logto/core/src/utils/secret-encryption.js';
import { SecretQueries } from '@logto/core/src/queries/secret.js';

// Retrieve and decrypt for API calls
const stored = await new SecretQueries().findById(secretId);
const tokenSet = decryptTokens(stored.secret);

// Use tokenSet.accessToken for the Authorization header
const response = await fetch('https://api.example.com/userinfo', {
  headers: { Authorization: `Bearer ${tokenSet.accessToken}` },
});

```

## Summary

- **Two-layer encryption** – Logto uses a runtime KEK to encrypt per-secret DEKs, ensuring isolation between credentials.
- **AES-256-GCM** – All encryption uses authenticated encryption with 256-bit keys and random initialization vectors.
- **Automatic cleanup** – Database triggers in relation tables prevent orphaned secrets when connectors are deleted.
- **Environment enforcement** – The application refuses to start without `SECRET_VAULT_KEK`, preventing accidental plaintext storage.
- **Type-safe queries** – The `SecretQueries` class encapsulates all database operations with tenant isolation.

## Frequently Asked Questions

### What happens if SECRET_VAULT_KEK is missing?

Logto validates the `SECRET_VAULT_KEK` environment variable during startup in [[`GlobalValues.ts`](https://github.com/logto-io/logto/blob/main/GlobalValues.ts)](https://github.com/logto-io/logto/blob/master/packages/shared/src/node/env/GlobalValues.ts). If the variable is undefined or empty, the application throws a fatal error and exits. This prevents the system from running in an insecure state where secrets might be stored unencrypted or decryption might fail silently.

### How does Logto prevent orphaned secrets?

The platform uses foreign key relations and database triggers defined in the [split-secret-connector-relations-table migration](https://github.com/logto-io/logto/blob/master/packages/schemas/alterations/1.30.0-1751255436-split-secret-connector-relations-table.ts). When a social connector, enterprise SSO connector, or user identity is deleted, the associated trigger automatically removes the corresponding entry from the junction table and deletes the secret from the vault, maintaining referential integrity without requiring application-level cleanup code.

### What encryption algorithm does Logto use for secrets?

Logto uses **AES-256-GCM** (Advanced Encryption Standard with Galois/Counter Mode) for all secret operations. This provides authenticated encryption, meaning the decryption process verifies data integrity using the authentication tag before returning plaintext, preventing tampering attacks.

### Can I rotate the Key-Encryption-Key without data loss?

Yes, but it requires a re-encryption process. Since each secret stores its DEK encrypted by the KEK, you can decrypt all secrets using the old KEK, then re-encrypt the DEKs with a new KEK. The per-secret DEKs remain unchanged during this process, so the actual credential data stays intact. Logto does not currently provide an automated rotation utility, so this must be implemented as a custom migration script using the `decryptSecret` and `encryptSecret` functions from [[`secret-encryption.ts`](https://github.com/logto-io/logto/blob/main/secret-encryption.ts)](https://github.com/logto-io/logto/blob/master/packages/core/src/utils/secret-encryption.ts).