Vector Database Encryption for Enterprise AI

TL;DR — Vector databases store the semantic content of your sensitive documents. If an attacker extracts embeddings, they can reconstruct the original text through embedding inversion. Encrypt at rest with AES-256. Use CMEK (customer-managed encryption keys) so you control key lifecycle and revocation — Pinecone and Zilliz Cloud support CMEK; Weaviate and Qdrant do not. For platforms without CMEK, use application-layer encryption before write as a compensating control. Isolate tenants with the platform's native mechanism: Pinecone namespaces, Weaviate shard-level multi-tenancy, or pgvector PostgreSQL row-level security. Enforce access control at the database layer, not just the application layer. Audit every vector query with tenant context. Never store embeddings of PII without encryption.

Why Vector Database Security Matters

Teams invest in prompt filtering, output monitoring, and LLM access controls while the database storing their embedded intellectual property runs with default authentication, no audit logging, and minimal network segmentation. The vector database is where your most sensitive retrieved data lives — the semantic content of every document you've ingested.

If an attacker extracts your vector embeddings, they can perform embedding inversion — reconstructing the original text from the embedding vectors. Research has demonstrated that embeddings can be inverted to recover significant portions of the source text. An unencrypted vector database is a plaintext copy of your documents in a different representation.

Encryption Layers

Layer 1: Encryption at Rest

The baseline requirement. All major vector databases support AES-256 encryption at rest:

Platform At-Rest Encryption Default
Pinecone AES-256 Enabled by default
Zilliz Cloud (Milvus) AES-256 Enabled by default
Weaviate AES-256 (self-hosted: full-disk encryption) Configurable
Qdrant AES-256 Configurable
pgvector (PostgreSQL) TDE via PostgreSQL Configurable

What it protects: Data files on disk — if someone steals the disk image or accesses the storage layer, they get ciphertext, not plaintext.

What it doesn't protect: The database process itself. When the database is running, vectors are in plaintext in memory for similarity computation. An attacker with database process access can read plaintext vectors.

Layer 2: Customer-Managed Encryption Keys (CMEK)

CMEK gives you control over the root encryption key. The database encrypts your data with a key hierarchy rooted in your KMS key:

Your KMS Key (AWS KMS / GCP KMS / Azure Key Vault)
    ↓ encrypts
Encryption Zone Key (EZK) — unique per database
    ↓ encrypts
Data Encryption Keys (DEKs) — per file
    ↓ encrypts
Vector data, indexes, logs

Three-tier key hierarchy benefits:
- If a single DEK is compromised, damage is contained to one file
- Key rotation rewraps the EZK without re-encrypting all data
- Revoking your KMS key immediately makes all data inaccessible to the database

Platform CMEK Support KMS Providers
Pinecone Yes (Enterprise plan) AWS KMS
Zilliz Cloud Yes (Business-Critical) AWS KMS
Weaviate Cloud BYOK (Enterprise tier) Limited
Qdrant No
pgvector Via PostgreSQL TDE AWS KMS, GCP KMS, Azure Key Vault

When CMEK is not available: Use application-layer encryption (Layer 3) as a compensating control. Auditors will flag the lack of CMEK and expect to see application-layer encryption with key management outside the vector database infrastructure.

Layer 3: Application-Layer Encryption

Encrypt vectors before writing them to the database. The database stores ciphertext. On read, decrypt after retrieval.

from cryptography.hazmat.primitives.ciphers.aead import AESGCM
import os

class VectorEncryptor:
    def __init__(self, key_provider: KeyProvider):
        self.key_provider = key_provider

    def encrypt_vector(self, vector: list[float], tenant_id: str) -> bytes:
        key = self.key_provider.get_key(tenant_id)
        aesgcm = AESGCM(key)
        nonce = os.urandom(12)

        # Convert vector to bytes
        import struct
        vector_bytes = struct.pack(f'{len(vector)}f', *vector)

        # Encrypt with tenant_id as associated data (prevents context swapping)
        ciphertext = aesgcm.encrypt(nonce, vector_bytes, tenant_id.encode())

        return nonce + ciphertext

    def decrypt_vector(self, encrypted: bytes, tenant_id: str) -> list[float]:
        key = self.key_provider.get_key(tenant_id)
        aesgcm = AESGCM(key)

        nonce = encrypted[:12]
        ciphertext = encrypted[12:]

        # Decrypt — fails if tenant_id doesn't match (context-swapping protection)
        vector_bytes = aesgcm.decrypt(nonce, ciphertext, tenant_id.encode())

        import struct
        count = len(vector_bytes) // 4
        return list(struct.unpack(f'{count}f', vector_bytes))

Limitation: The database cannot compute similarity on encrypted vectors. You must decrypt before similarity search, which means the database sees plaintext vectors in memory during query execution. This is not a replacement for CMEK — it's a compensating control.

When to use:
- Platform doesn't support CMEK (Qdrant, self-hosted Weaviate)
- You want protection against database administrator access
- You need per-tenant encryption keys that the database doesn't natively support

Associated Data (AAD) binding: Use the tenant_id as AAD in AES-GCM. This prevents context-swapping attacks — an attacker cannot take ciphertext from tenant A and use it in tenant B's context, because the AAD won't match.

Layer 4: Encryption in Transit

All connections to the vector database must use TLS 1.2+ (preferably TLS 1.3). This is non-negotiable for enterprise deployments.

# Pinecone — TLS is automatic
from pinecone import Pinecone
pc = Pinecone(api_key="...", ssl=True)

# pgvector — require SSL
import psycopg2
conn = psycopg2.connect(
    "postgresql://user:pass@host/db?sslmode=require"
)

# Weaviate — HTTPS
import weaviate
client = weaviate.connect_to_local(
    host="localhost",
    port=8080,
    grpc_port=50051,
    skip_init_checks=True  # Only for dev — never in production
)

Tenant Isolation

Pinecone: Namespace-Based Isolation

Each tenant gets a namespace. Queries are scoped to a single namespace per call.

# Write to tenant's namespace
index.upsert(
    vectors=[("vec1", [0.1, 0.2, ...], {"source": "doc1"})],
    namespace="tenant-456"
)

# Query only tenant's namespace
results = index.query(
    vector=[0.1, 0.2, ...],
    top_k=10,
    namespace="tenant-456"  # Critical: never omit this
)

Security: The isolation depends on your application correctly scoping every query to the right namespace. A bug that omits the namespace parameter could query the wrong tenant's data.

CMEK enhancement: With CMEK, each tenant's data is encrypted with different keys, enhancing isolation even within the multi-tenant environment.

Weaviate: Shard-Level Multi-Tenancy

Each tenant's data is isolated into a separate physical shard. Data in one shard is not accessible to queries scoped to a different tenant at the storage layer.

# Create tenant
client.collections.get("Document").tenants.create([
    Tenant(name="tenant-456", activity_status=TenantActivityStatus.ACTIVE)
])

# Query tenant's data — automatically scoped to tenant's shard
collection = client.collections.get("Document")
tenant_collection = collection.with_tenant("tenant-456")
results = tenant_collection.query.near_vector(
    query_vector=[0.1, 0.2, ...],
    limit=10
)

Security: Strongest isolation among the three platforms. A compromised retrieval service can only access the shards assigned to its tenant. Weaviate supports 50,000+ active shards per node.

RBAC: Weaviate v1.29.0+ introduced fine-grained RBAC at the collection level. Earlier versions only had Admin and Read-Only roles — check your version.

pgvector: PostgreSQL Row-Level Security

Each row carries a tenant_id. RLS policies filter queries automatically at the database engine level.

-- Enable RLS
ALTER TABLE embeddings ENABLE ROW LEVEL SECURITY;

-- Create policy
CREATE POLICY tenant_isolation ON embeddings
    USING (tenant_id = current_setting('app.tenant_id'));

-- Set tenant context per session
SET app.tenant_id = 'tenant-456';

-- All queries automatically filtered to tenant-456
SELECT id, embedding <=> '[0.1, 0.2, ...]' AS distance
FROM embeddings
ORDER BY distance
LIMIT 10;
-- Only returns rows where tenant_id = 'tenant-456'

Security: RLS is a well-established, auditable PostgreSQL feature. It applies to all query types including vector similarity searches using pgvector's distance operators (<=> for cosine, <-> for L2). The policy is applied before any data is returned, including the similarity computation.

Verification: Use EXPLAIN (ANALYZE, VERBOSE) to see the RLS filter in the query plan — auditors can verify enforcement at the engine level.

Important: The retrieval_service role must not have the BYPASSRLS attribute. Any role with BYPASSRLS can read all tenants' data regardless of policies.

Per-Tenant Encryption Keys

For maximum isolation, use per-tenant encryption keys so that one tenant's data cannot be decrypted with another tenant's key:

class TenantKeyProvider:
    def __init__(self, kms_client):
        self.kms = kms_client
        self.cache = {}  # tenant_id -> decrypted key (in-memory only)

    def get_key(self, tenant_id: str) -> bytes:
        if tenant_id in self.cache:
            return self.cache[tenant_id]

        # Get tenant's encrypted DEK from KMS
        encrypted_dek = self.kms.get_tenant_dek(tenant_id)

        # Decrypt using tenant's KEK (which is wrapped by the master KMS key)
        dek = self.kms.decrypt(encrypted_dek)

        self.cache[tenant_id] = dek
        return dek

    def revoke_tenant(self, tenant_id: str):
        """Revoke a tenant's access by destroying their key."""
        self.kms.schedule_key_destruction(tenant_id)
        del self.cache[tenant_id]
        # All data encrypted with this key is now unreadable

GDPR right to erasure: Destroying a tenant's encryption key effectively erases their data — even if the ciphertext remains on disk, it cannot be decrypted. This is faster and more thorough than deleting individual vectors.

Audit Logging

Vector database audit logs must capture every query with tenant context:

{
  "timestamp": "2026-07-08T10:15:00Z",
  "user_id": "alice@company.com",
  "tenant_id": "tenant-456",
  "operation": "vector_search",
  "query_vector_hash": "sha256:a1b2c3...",
  "top_k": 10,
  "namespace": "tenant-456",
  "results_returned": 8,
  "result_ids": ["vec-1", "vec-2", "vec-3"],
  "execution_time_ms": 12,
  "filters_applied": {"source": "knowledge_base"}
}

Platform-specific audit logging:

Platform Audit Mechanism
Pinecone API logs with namespace context
Weaviate Built-in audit logging (v1.29.0+)
Qdrant Collection-level access logs
pgvector pgaudit extension with pgaudit.log = 'read, write'

Critical: Verify that every query log entry includes the tenant field. A missing tenant field in a log entry can indicate a namespace escape condition.

Platform Comparison

Feature Pinecone Weaviate pgvector Qdrant
At-rest encryption AES-256 (default) AES-256 (configurable) TDE via PostgreSQL AES-256
CMEK Yes (Enterprise, AWS) BYOK (Enterprise tier) Via PostgreSQL TDE No
Tenant isolation Namespaces Shard-level multi-tenancy Row-level security Payload filtering
RBAC API key roles + user roles Fine-grained (v1.29.0+) PostgreSQL roles + RLS JWT-based (v1.9.0+)
Audit logging API logs Built-in (v1.29.0+) pgaudit extension Collection-level logs
Compliance SOC 2 Type II SOC 2 (cloud tier) Inherits PostgreSQL certs SOC 2
Self-hosted No Yes Yes Yes

Common Security Mistakes

Default Authentication

Running the vector database with default credentials or no authentication. This is the most common finding in security reviews.

Fix: Enable authentication. Use strong, unique credentials. Rotate regularly.

Over-Permissioned Ingestion Credentials

The ingestion pipeline has delete and schema-modification access that it doesn't need. A compromised ingestion service can destroy all vectors.

Fix: Principle of least privilege. Ingestion needs INSERT and READ only. Separate roles for schema management and deletion.

No Tenant Context in Queries

Forgetting to pass the namespace (Pinecone), tenant (Weaviate), or tenant_id setting (pgvector) in a query. The query runs against all tenants' data.

Fix: Centralize all database access through a gateway that automatically injects tenant context. Never allow direct database access from application code.

Embedding PII Without Encryption

Embedding documents containing PII and storing the vectors unencrypted. The embeddings can be inverted to recover the PII.

Fix: Redact PII before embedding. If you must embed PII, encrypt the vectors at rest with CMEK or application-layer encryption.

No Audit Logging

No record of who queried what, when, and which results were returned. You cannot investigate incidents or prove compliance.

Fix: Enable audit logging on all platforms. Log every query with user, tenant, and result context. Send logs to a SIEM for correlation and alerting.

Implementation Checklist

  • [ ] Enable AES-256 encryption at rest on all vector databases
  • [ ] Enable TLS 1.2+ for all connections (preferably TLS 1.3)
  • [ ] Configure CMEK where available (Pinecone Enterprise, Zilliz Business-Critical)
  • [ ] Implement application-layer encryption where CMEK is not available (Qdrant, self-hosted Weaviate)
  • [ ] Use per-tenant encryption keys for maximum isolation
  • [ ] Use tenant_id as AAD in AES-GCM to prevent context-swapping
  • [ ] Implement tenant isolation using platform-native mechanism (namespaces, shards, or RLS)
  • [ ] Centralize all database access through a gateway that injects tenant context
  • [ ] Never allow direct database access from application code
  • [ ] Apply principle of least privilege to database roles
  • [ ] Separate ingestion (INSERT) from management (DELETE, schema) roles
  • [ ] Enable audit logging on all platforms
  • [ ] Verify every log entry includes tenant context
  • [ ] Send audit logs to SIEM for correlation and alerting
  • [ ] Redact PII before embedding — don't embed what you don't need
  • [ ] Encrypt vectors of sensitive documents even if CMEK is enabled (defense in depth)
  • [ ] Test tenant isolation with adversarial queries (try to access other tenants' data)
  • [ ] Verify RLS enforcement with EXPLAIN (ANALYZE, VERBOSE) for pgvector
  • [ ] Implement key rotation with zero downtime
  • [ ] Document encryption architecture for compliance audits

Conclusion

Vector databases store the semantic content of your most sensitive documents. An unencrypted vector database is a plaintext copy of your data in a different representation — and embedding inversion attacks can reconstruct the original text from stored vectors.

Encrypt at rest with AES-256. Use CMEK where available so you control key lifecycle and revocation. For platforms without CMEK, use application-layer encryption with per-tenant keys and AAD binding. Isolate tenants using the platform's native mechanism — Pinecone namespaces, Weaviate shard-level multi-tenancy, or pgvector row-level security. Centralize all database access through a gateway that injects tenant context. Never allow direct database access. Audit every query with tenant context.

Enterprise buyers will ask about encryption, tenant isolation, and audit logging in their security review. Having clear answers — CMEK, per-tenant keys, platform-native isolation, and comprehensive audit logging — is essential for passing enterprise procurement.