Free lesson · GenAI Security Engineering

Deploy canary documents for tampering detection

Generate synthetic canary documents with known embeddings. Build verification systems that detect when canary retrieval results change.

Course: AI Security Engineering · Chapter 6 · RAG Data Poisoning Defense

Free to read — no subscription required.

Introduction

In production, a RAG knowledge base can be silently poisoned — documents modified, deleted, or displaced — with no immediate alarm to show for it. Embedding drift metrics catch broad statistical shifts, but a targeted attack on just a handful of documents can fly entirely under that radar. Canary documents solve this by planting synthetic tripwires with precisely known embeddings throughout the knowledge base: any deviation from their expected retrieval behavior is immediate, deterministic proof of tampering. By the end of this lesson, you'll be able to generate realistic canary documents, deploy them across semantic clusters, and run automated verification loops that detect displacement attacks within seconds.

Key Terminology

  • Canary Document: A synthetic document with pre-computed embeddings planted in a vector store to detect unauthorized modifications, deletions, or displacement attacks.
  • Displacement Attack: A poisoning technique where an adversary injects documents designed to push legitimate content (and canaries) lower in retrieval rankings without modifying existing documents.
  • Canary Rotation: The periodic process of replacing deployed canary documents with newly generated ones to prevent adversaries from identifying and avoiding them.
  • Retrieval Rank Verification: The check that confirms a canary document still appears within the expected top-K results when queried with its own embedding vector.
  • Semantic Cluster Coverage: A deployment strategy that places canary documents across all identified topic clusters in a knowledge base to ensure no semantic region is unmonitored.

When canary verification fails, the immediate next step is to invoke the quarantine pipeline for all documents sharing ingestion timestamps or provenance sources with the compromised region. This bridges canary detection into the broader RAG integrity validation workflow, ensuring that a detected tripwire activation translates into concrete containment action rather than merely an alert in a dashboard.

Concepts

Canary Deployment Strategy

Effective canary placement requires distributing canaries across the semantic space of the knowledge base, not clustering them in a single topic region. A recommended strategy is to deploy one canary per semantic cluster identified through k-means or HDBSCAN clustering of the existing corpus embeddings. This ensures that poisoning attacks targeting any topic area will encounter at least one canary. For a knowledge base with 10,000 documents across 50 semantic clusters, deploying 50 canaries—one per cluster—provides comprehensive coverage at a cost of only 0.5% additional storage.

  • Density Rule: Deploy at least one canary per semantic cluster. For high-value clusters (those containing sensitive or frequently queried content), deploy two or three canaries at different positions within the cluster.
  • Rotation Schedule: Rotate canary content every 7-14 days to prevent attackers from building a static model of which documents are canaries. Rotation involves generating a new canary, deploying it, verifying the old canary one final time, and then removing it.
  • Verification Frequency: Run verification every 60 seconds for high-security deployments, every 5 minutes for standard deployments. Each verification cycle should complete within 10% of the interval to avoid overlapping runs.
  • Alert Escalation: A single DISPLACED result should trigger a warning-level alert. Two or more DISPLACED canaries in the same verification cycle should trigger a critical alert and automatically engage the quarantine pipeline. Any MODIFIED or MISSING canary should always trigger a critical alert.
Loading diagram...

Code Walkthrough

Now that you understand the canary deployment strategy — one canary per semantic cluster, verified on a tight schedule, rotated every 7–14 days — here is the Python implementation.

The CanaryDocument dataclass captures everything the verification loop needs: the pre-computed embedding baseline, the content hash for stable identity, the deployment region, and running verification statistics. The generate_canary function interpolates domain vocabulary into a template, hashes the result for a stable canary_id, and calls the production embedding function once to record the expected vector. Blending domain vocabulary into the template content is what makes canaries indistinguishable from legitimate documents to an attacker scanning the vector store.

Code snippetpython
1import hashlib 2import time 3from dataclasses import dataclass, field 4from typing import Optional, Callable 5import numpy as np 6 7@dataclass 8class CanaryDocument: 9 canary_id: str 10 content: str 11 content_hash: str 12 expected_embedding: Optional[np.ndarray] = None 13 deployed_at: float = field(default_factory=time.time) 14 region: str = "default" 15 last_verified: Optional[float] = None 16 verification_count: int = 0 17 18def generate_canary( 19 region: str, 20 domain_vocab: list[str], 21 embed_fn: Callable[[str], np.ndarray], 22) -> CanaryDocument: 23 import random 24 topic = random.choice(domain_vocab) 25 detail = random.choice(domain_vocab) 26 rev = hashlib.sha256(f"{topic}{time.time()}".encode()).hexdigest()[:8] 27 content = ( 28 f"Internal reference document: {topic} — revision {rev}. " 29 f"This document covers {detail} as part of standard operations. " 30 f"Classification: general reference. Status: active." 31 ) 32 content_hash = hashlib.sha256(content.encode()).hexdigest() 33 return CanaryDocument( 34 canary_id=f"canary-{content_hash[:12]}", 35 content=content, 36 content_hash=content_hash, 37 expected_embedding=embed_fn(content), 38 region=region, 39 )

With canaries deployed, the verification loop queries the vector store using each canary's own embedding and checks whether the canary still ranks within the expected top-K results. A MISSING result means the document was deleted or its embedding overwritten — a direct modification attack. A DISPLACED result means a displacement attack pushed the canary below the retrieval threshold without touching the document itself. Two or more compromised canaries in a single cycle escalates to critical and triggers the quarantine pipeline for all co-ingested documents.

Code snippetpython
1def verify_canary( 2 canary: CanaryDocument, 3 query_fn: Callable[[np.ndarray, int], list[dict]], 4 top_k: int = 10, 5) -> str: 6 results = query_fn(canary.expected_embedding, top_k) 7 result_ids = [r["id"] for r in results] 8 canary.last_verified = time.time() 9 canary.verification_count += 1 10 if canary.canary_id not in result_ids: 11 return "MISSING" 12 return "DISPLACED" if result_ids.index(canary.canary_id) >= top_k // 2 else "PASS" 13 14def run_verification_cycle( 15 canaries: list[CanaryDocument], 16 query_fn: Callable[[np.ndarray, int], list[dict]], 17) -> dict[str, str]: 18 statuses: dict[str, str] = {} 19 alert_count = 0 20 for canary in canaries: 21 status = verify_canary(canary, query_fn) 22 statuses[canary.canary_id] = status 23 if status != "PASS": 24 alert_count += 1 25 print(f"[ALERT] {canary.canary_id} ({canary.region}): {status}") 26 if alert_count >= 2: 27 print("[CRITICAL] Multiple canaries compromised — invoke quarantine pipeline.") 28 return statuses

The escalation threshold in run_verification_cycle maps directly to the alert rules from the Concepts section: one displaced canary raises a warning, two or more triggers the critical path and quarantine. Schedule run_verification_cycle every 60 seconds for high-security deployments, every 5 minutes for standard ones, and replace canary content every 7–14 days to prevent attackers from building a static fingerprint of which documents are canaries.

Confirm that run_verification_cycle returns all PASS statuses against an unmodified index, and that removing a canary document from the store causes its next cycle to return MISSING for that canary.

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

  1. Do blend domain vocabulary into canary content using generate_canary's template interpolation — canaries that read as generic synthetic text are trivially identified by an attacker scanning the vector store; domain-realistic phrasing makes them indistinguishable from legitimate documents, preserving the tripwire's stealth.
  2. Do deploy one canary per semantic cluster and verify on a fixed schedule (every 60 seconds for high-security, every 5 minutes for standard) — sparse coverage lets a targeted attack slip between canaries, and infrequent polling widens the window between a displacement attack and detection.
  3. Do escalate to the quarantine pipeline when two or more canaries return MISSING or DISPLACED in a single run_verification_cycle call — a single displaced canary may be noise, but simultaneous compromise across multiple regions is deterministic proof of a coordinated poisoning event that warrants quarantining all co-ingested documents.

Don'ts

  1. Don't reuse static canary content beyond 7–14 days — a canary whose content never changes gives an attacker enough observation time to fingerprint which documents are tripwires, allowing them to route the poisoning attack around the known canaries while leaving verification returning PASS.
  2. Don't rely solely on embedding drift metrics to detect targeted poisoning — drift monitors catch broad statistical shifts, but a surgical attack on a handful of documents produces no measurable population-level drift; only verify_canary querying each canary's expected_embedding against the live index can catch displacement of individual documents.
  3. Don't evaluate canary presence by content-hash lookup instead of retrieval rank — checking whether a document exists in the store misses displacement attacks entirely; verify_canary must issue a live query_fn(canary.expected_embedding, top_k) and confirm the canary ranks in the top half of results, because displacement pushes it below the retrieval threshold without deleting or modifying the document.

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

All free lessons in GenAI Security Engineering