Free lesson · GenAI Security Engineering
Implement embedding integrity and provenance tracking
Build embedding generation audit logging, version registry, and drift detection against baseline snapshots.
Course: AI Security Engineering · Chapter 9 · Embedding & Vector Store Security
Free to read — no subscription required.
Introduction
In production RAG pipelines, an attacker who gains write access to the embedding pipeline—through a compromised CI/CD step, a malicious dependency in an embedding container, or a lateral move within a shared GKE cluster—can silently replace legitimate vectors with adversarial ones without modifying the source documents. Row-level access control and query injection prevention don't catch this because the tampering occurs after those gates are passed. This lesson teaches you how to build a version registry that records SHA-256 fingerprints and provenance metadata at embedding generation time, validates stored vectors before they reach the LLM context window, and provides a full audit trail of model version, source document, tenant, and batch for every vector in your store.
Key terminology
- Embedding provenance: The full lineage of a vector, including the source document hash, the model identifier, the model version, and the generation timestamp.
- Version registry: A tamper-evident ledger that stores metadata and cryptographic hashes for every embedding batch, enabling integrity verification and rollback.
- Registration-before-storage: The ordering invariant that an embedding's fingerprint and provenance must be recorded in the registry before the vector is written to the store, so any later integrity check has an authoritative reference to compare against.
- Embedding fingerprint: A SHA-256 hash computed over the raw float array of a vector, used to verify that the embedding has not been altered since registration.
- Trust domain separation: Keeping the integrity registry in a different trust domain from the vector store, so an attacker who tampers with stored vectors cannot also forge the fingerprints used to detect the tampering.
Concepts
The Control Gap Fingerprinting Fills
Row-level access control and query injection prevention are request-boundary controls — they operate at the moment a principal reads from or writes to the vector store. Neither sees what happens to vectors after a legitimate write completes. An attacker with write access to the embedding pipeline — through a compromised CI/CD step, a malicious layer in the embedding container image, or a lateral move within a shared GKE namespace — can replace stored vectors with adversarial ones without touching a single source document. By the time the retrieval path runs, the row-level gate has already been satisfied and there is no malicious query to inspect. Cryptographic fingerprinting answers a different question than those controls: not "who is allowed to access this?" but "is what's stored now byte-for-byte identical to what was registered at generation time?"
Why Registration Must Precede Vector Storage
The registry is only meaningful if the fingerprint is recorded before the vector reaches the vector store. register_embedding builds the EmbeddingRecord — including the vector_fingerprint — and persists it first; the vector database write happens afterward. This ordering is load-bearing: a fingerprint computed after storage would miss any tamper event that occurs in the interval between write and hash. The normalization step inside _compute_fingerprint — vector.astype(np.float32).tobytes() — is equally important: vectors travel through multiple serialization layers, and a float64 representation of the same logical values would produce a different SHA-256 digest, causing legitimate vectors to fail verification. Pinning to float32 before hashing makes the digest stable regardless of the dtype in which the vector arrives at query time (see Code Walkthrough).
Separate Trust Domains Are What Give the Check Its Adversarial Strength
The VersionRegistry stores records in a different location than the vector database. The code walkthrough uses an in-memory dictionary; in production this maps to an append-only PostgreSQL table protected by row-level security. The critical design principle is that the registry must live in a trust domain that is disjoint from the vector store: an attacker who compromises only the vector store cannot reach the fingerprints in the registry, so verify_integrity still detects the substitution. For the check to be defeated, an attacker must compromise both stores simultaneously. The get_provenance method reinforces this separation by exposing the full audit record — source document hash, model identifier, model version, tenant, timestamp, and batch ID — so that a False integrity result produces enough context to identify which pipeline run and tenant namespace produced the suspect vector, rather than just signaling that something is wrong.
Code Walkthrough
Now that you understand how the version registry sits between the embedding pipeline and the vector store, the implementation centers on two components: an EmbeddingRecord dataclass that captures provenance for a single vector, and a VersionRegistry class that manages registration and integrity validation.
_compute_fingerprint normalizes the input array to float32 bytes before hashing, making the digest deterministic across NumPy dtype variations. _hash_document hashes source content so the registry ties each vector to the exact text that produced it, not just a storage identifier. register_embedding creates a fully populated record and persists it before the vector reaches the vector database. The storage here uses an in-memory dictionary; in production this maps to an append-only PostgreSQL table protected by row-level security so an attacker who compromises the vector store cannot also alter the registry.
Code snippetpython
1import hashlib 2import numpy as np 3from dataclasses import dataclass 4from datetime import datetime, timezone 5from typing import Optional 6 7@dataclass 8class EmbeddingRecord: 9 embedding_id: str 10 source_doc_hash: str 11 model_id: str 12 model_version: str 13 vector_fingerprint: str 14 dimension: int 15 tenant_id: str 16 created_at: str 17 batch_id: Optional[str] = None 18 19class VersionRegistry: 20 def __init__(self): 21 self._records: dict[str, EmbeddingRecord] = {} 22 23 def _compute_fingerprint(self, vector: np.ndarray) -> str: 24 return hashlib.sha256(vector.astype(np.float32).tobytes()).hexdigest() 25 26 def _hash_document(self, content: str) -> str: 27 return hashlib.sha256(content.encode("utf-8")).hexdigest() 28 29 def register_embedding( 30 self, 31 embedding_id: str, 32 vector: np.ndarray, 33 source_content: str, 34 model_id: str, 35 model_version: str, 36 tenant_id: str, 37 batch_id: Optional[str] = None, 38 ) -> EmbeddingRecord: 39 record = EmbeddingRecord( 40 embedding_id=embedding_id, 41 source_doc_hash=self._hash_document(source_content), 42 model_id=model_id, 43 model_version=model_version, 44 vector_fingerprint=self._compute_fingerprint(vector), 45 dimension=len(vector), 46 tenant_id=tenant_id, 47 created_at=datetime.now(timezone.utc).isoformat(), 48 batch_id=batch_id, 49 ) 50 self._records[embedding_id] = record 51 return record 52 53 def verify_integrity(self, embedding_id: str, vector: np.ndarray) -> bool: 54 record = self._records.get(embedding_id) 55 if record is None: 56 return False 57 return self._compute_fingerprint(vector) == record.vector_fingerprint 58 59 def get_provenance(self, embedding_id: str) -> Optional[EmbeddingRecord]: 60 return self._records.get(embedding_id)
At retrieval time, pass the fetched vector back into verify_integrity. Because the fingerprint is computed deterministically from raw float bytes, any modification—intentional tampering or hardware corruption—produces a different SHA-256 digest and returns False. A False result should route the vector to a quarantine path rather than silently serving it to the LLM context window. The get_provenance method returns the full audit record: source document hash, model identifier, model version, tenant, timestamp, and batch ID—enough to identify exactly which pipeline run and tenant namespace produced a suspect vector.
Code snippetpython
1registry = VersionRegistry() 2vector = np.array([0.12, -0.34, 0.56, 0.78], dtype=np.float32) 3 4record = registry.register_embedding( 5 embedding_id="doc-001", 6 vector=vector, 7 source_content="Kubernetes pod security policy for namespace isolation.", 8 model_id="text-embedding-3-small", 9 model_version="1.0.0", 10 tenant_id="tenant-acme", 11) 12print(f"Registered fingerprint: {record.vector_fingerprint[:16]}…") 13 14# Integrity check passes for the original vector 15assert registry.verify_integrity("doc-001", vector) 16 17# Integrity check fails when any element is modified 18tampered = vector.copy() 19tampered[0] += 0.001 20assert not registry.verify_integrity("doc-001", tampered) 21print("Tampered vector correctly rejected.") 22 23# Full provenance available for audit 24prov = registry.get_provenance("doc-001") 25print(f"Model: {prov.model_id} v{prov.model_version}, tenant: {prov.tenant_id}")
Confirm that verify_integrity returns True for the original vector, False when any element is modified, and False when queried with an unregistered embedding ID.
Do's and Don'ts
Now that you have worked through the implementation, the practices below separate a durable approach from a fragile one.
Do's
- ✓Do call
register_embeddingbefore the vector is written to the vector database — recording the SHA-256 fingerprint andsource_doc_hashat generation time is what makes later tampering detectable; registering after the write creates a window where an adversarial vector can be stored with a fingerprint that matches it. - ✓Do cast the vector to
float32viavector.astype(np.float32).tobytes()before hashing —_compute_fingerprintdoes this deliberately to produce a deterministic digest regardless of the NumPy dtype the upstream pipeline uses; skipping the cast means the same logical vector can produce different fingerprints across dtype variations and cause false integrity failures. - ✓Do route a
Falseresult fromverify_integrityto a quarantine path instead of serving the vector — a non-matching digest means either intentional tampering (adversarial vector injection through a compromised CI/CD step or embedding container) or hardware corruption; silently passing the vector to the LLM context window defeats the entire purpose of the registry.
Don'ts
- ✗Don't store the
VersionRegistryin the same datastore as the vector embeddings — the in-memory dictionary in this lesson maps to an append-only PostgreSQL table protected by row-level security specifically because an attacker who compromises the vector store must not also be able to alter the fingerprint records; co-locating both gives a single write-access exploit the ability to update both the vector and its storedvector_fingerprintto match. - ✗Don't use only a storage identifier (e.g., a document ID) as the source reference in
EmbeddingRecord—_hash_documenthashes the raw source content so the registry ties each vector to the exact text that produced it; replacing the hash with an opaque ID means a tampered pipeline can swap source content while the ID stays unchanged andsource_doc_hashprovides no signal. - ✗Don't skip querying
get_provenancewhenverify_integrityreturnsFalse— the full audit record (model_id,model_version,tenant_id,batch_id,created_at) is what identifies which pipeline run and tenant namespace produced the suspect vector; discarding that context on a failed check turns a traceable incident into an opaque rejection with no investigative trail.
This lesson is free to read. Its 3 hands-on labs — real code, in a cloud IDE — are part of the GenAI Security Engineering subscription.
From · cancel anytime · Already a subscriber? Sign in →
More free lessons in AI Security Engineering
- Ch 8Integrate PII defense with LiteLLM gateway
- Ch 8Deploy PII defense pipeline on GKE
- Ch 9Implement row-level access control for vector stores
- Ch 9Implement embedding integrity and provenance trackingYou are here
- Ch 9Encrypt embeddings at rest and in transit
- Ch 9Deploy secure vector store on GKE with network isolation
- Ch 9Monitor vector store access patterns and anomalies