Model Inversion & Knowledge Base Extraction Defence
This page covers an important distinction: classical model inversion is low risk for this stack, but two closely related attacks are real and require active controls.
Classical Model Inversion β Why It's Low Risk Hereβ
Classical model inversion attacks query a model repeatedly to reconstruct its training data from its weights β extracting memorised private text that the model saw during training. This is well-documented against large language models where researchers extracted verbatim training samples via specific prompting.
This is low risk for this specific deployment because:
llama3.2:1b and llama3.2:3b are pre-trained base models from Meta, trained on public web data. HDI Seguros policy documents, claim records, underwriting criteria, and financial data are not in that training set β they are confidential internal documents that never reached the public internet. There is nothing to extract from the model weights that relates to HDI Seguros business.
The assessment changes completely if models are ever fine-tuned on internal documents. See the Fine-Tuning Gate section β this is a hard prerequisite before any fine-tuning phase.
The Two Real Attacks for RAG Systemsβ
Attack 1 β Knowledge Base Enumerationβ
An attacker with a valid virtual key systematically queries the system to reconstruct the entire Qdrant knowledge base without ever accessing Qdrant directly:
"What is the franchise for water damage?" β extracts β¬150
"What is the franchise for fire damage?" β extracts β¬500
"What is the indemnity limit for property damage?" β extracts β¬500,000
"What are the exclusions for clause 1?" β extracts clause text
"What are the exclusions for clause 2?" β extracts clause text
...200 queries later: full policy wording reconstructed
Each individual query is legitimate. Only the systematic pattern across many distinct topics reveals the attack. This requires no technical exploit β just a valid API key and a structured list of questions.
Risk in insurance: entire policy wordings, pricing structures, underwriting criteria, and claims procedures could be exfiltrated by a competitor, a disgruntled employee, or a compromised broker account. All of this is confidential commercial and regulatory information.
Attack 2 β Embedding Reconstructionβ
nomic-embed-text embeddings (768-dimensional vectors) are stored in Qdrant alongside the original chunk text. Research (Vec2Text, 2023) has demonstrated that embedding vectors can be inverted to reconstruct the original text with meaningful accuracy for sentence-length inputs (~40β60% fidelity).
If Qdrant storage is compromised, an attacker with the raw embedding vectors could partially recover policy document text without having the documents themselves.
Risk level in this stack: lower than enumeration, because Qdrant is internal-only β it has no Ingress, is accessible only via Tailscale and Cilium NetworkPolicy, and requires a network-level compromise first. Mitigated by Longhorn LUKS encryption at rest.
What's Already In Your Stackβ
| Control | What it covers |
|---|---|
| Virtual key routing (issue #34) | Department-scoped read access β IT key cannot query claims-docs |
| Per-key query rate limits (issue #34) | Slows enumeration β limited requests per minute |
| DataSovereigntyHook (issue #42) | Responses never leave your infrastructure |
| Tailscale + UFW | Qdrant unreachable from internet |
| Cilium NetworkPolicy | Pod-level network isolation within cluster |
Virtual key routing already gives partial enumeration resistance β a compromised direction-it key can only enumerate the IT collection, not claims or finance. The blast radius of any single compromised key is scoped to one department's document set.
Defence Architectureβ
PRE-CALL (EnumerationDetectionHook)
βββ Track unique query topic fingerprints per API key per hour
β warning at 60 distinct topics
β block + CRITICAL event at 80 distinct topics
POST-CALL (verbatim reproduction check)
βββ SequenceMatcher: response vs retrieved chunks
β > 60% verbatim overlap β abstraction_compliance = 0 + WARNING
SYSTEM PROMPT (high-stakes departments)
βββ Abstraction constraint appended to claims/finance/legal/underwriting prompts
"Ne jamais reproduire textuellement plus de deux phrases consΓ©cutives"
STORAGE (Qdrant PVC)
βββ Longhorn LUKS encryption at rest
β embedding vectors + chunk text encrypted on disk
β key managed by Vault (issue #15)
FINE-TUNING GATE
βββ GitHub Actions workflow blocks fine-tuning unless safety checklist closed
Layer 1 β Enumeration Detectionβ
How it worksβ
Each query is fingerprinted by its first six normalised words. Redis HyperLogLog tracks the cardinality of unique fingerprints per API key per hour β the count of distinct topics, not the count of queries.
| Fingerprint A | "what is the franchise for water" |
|---|---|
| Fingerprint B | "what is the franchise for fire" |
| Fingerprint C | "what is the indemnity limit for" |
Three distinct topics. The same question asked 80 times counts as one topic β normal repeated clarifications do not trigger the detector.
Thresholdsβ
| Threshold | Action |
|---|---|
| 60 unique topics / hour | enumeration-warning WARNING event in Langfuse β monitoring escalated, request not blocked |
| 80 unique topics / hour | enumeration-detected ERROR event + HTTP 429 β request blocked, Grafana alert fires |
Counter resets after 1 hour. A power user who legitimately asks many different questions will approach 60 but is unlikely to exceed 80 in a single hour of normal work β the threshold is calibrated to systematic machine-speed querying, not human browsing.
Reviewing an enumeration alertβ
Langfuse β Events β filter: name = "enumeration-detected"
β metadata.key_hash identifies the API key
β metadata.unique_topics count at time of block
β metadata.department which collection was being enumerated
Cross-reference with Langfuse Traces to see the full query sequence for that key hash:
Langfuse β Traces β filter by time window β sort by created_at
β look for short factual questions with systematic topic progression
If enumeration is confirmed, revoke the key:
curl -X DELETE http://litellm.ai.svc:4000/key/delete \
-H "Authorization: Bearer $LITELLM_MASTER_KEY" \
-d '{"keys": ["<virtual-key>"]}'
Layer 2 β Response Abstractionβ
System prompt constraintβ
All high-stakes department system prompts (claims, finance, legal, underwriting, audit) include:
RΓGLE SUPPLΓMENTAIRE β REPRODUCTION TEXTUELLE:
Ne jamais reproduire textuellement plus de deux phrases consΓ©cutives d'un document source.
SynthΓ©tise, reformule et explique β ne copie pas le texte brut du document.
Si l'utilisateur demande explicitement une citation textuelle, indique:
"Je ne peux pas reproduire le texte intΓ©gral. RΓ©fΓ©rez-vous au document [source] page [X]."
This makes enumeration harder β the attacker gets a paraphrase, not the exact policy wording. Combined with the citation requirement from issue #44, the attacker knows which document to request but cannot reconstruct its text from model responses alone.
Post-call verbatim detectionβ
After every response, the abstraction_compliance score is computed using SequenceMatcher between the response text and each retrieved chunk:
| Score | Meaning |
|---|---|
| 1 | Response is a genuine synthesis β no verbatim copying detected |
| 0 | Response is > 60% verbatim overlap with a source chunk β flagged |
A score of 0 fires a verbatim-reproduction-detected WARNING in Langfuse. This is non-blocking β it surfaces cases where the model ignored the abstraction constraint for human review and system prompt tuning.
Layer 3 β Qdrant PVC Encryption at Restβ
Whyβ
Qdrant stores two sensitive artefacts on disk: the original chunk text (policy document excerpts) and the nomic-embed-text embedding vectors. Without encryption, a compromised node, a stolen disk, or a Longhorn snapshot exfiltration exposes both in plaintext. The embedding vectors can be partially inverted to recover text even without the chunk payload.
Implementation β Longhorn LUKSβ
Longhorn's built-in encryption support uses LUKS (Linux Unified Key Setup) at the volume level. The encryption key is stored in a Kubernetes Secret populated by the External Secrets Operator from Vault (issue #15) β never hardcoded.
# StorageClass: longhorn-encrypted
parameters:
encrypted: "true"
CRYPTO_KEY_CIPHER: "aes-xts-plain64"
CRYPTO_KEY_HASH: "sha256"
CRYPTO_KEY_SIZE: "256"
CRYPTO_PBKDF: "argon2i"
Qdrant's PVC uses storageClassName: longhorn-encrypted. The encryption is transparent to Qdrant β no application changes required.
Verifying encryption is activeβ
# On the node hosting the Qdrant PVC (check which node via kubectl)
ssh set-hog "lsblk -o NAME,TYPE,FSTYPE | grep crypto"
# β shows a crypto_LUKS device backing the volume
# From Mac via controller:
kubectl get pvc -n ai
# β qdrant-storage Bound longhorn-encrypted 10Gi
Migration from unencrypted PVCβ
# 1. Scale down Qdrant
kubectl scale deployment qdrant -n ai --replicas=0
# 2. Snapshot existing PVC via Longhorn UI
# β Longhorn UI β Volumes β qdrant-storage β Create Snapshot
# 3. Restore snapshot into a new PVC using longhorn-encrypted StorageClass
# β Longhorn UI β Snapshots β Restore β StorageClass: longhorn-encrypted
# 4. Update Qdrant Helm values
ssh controller "/home/ktayl/.local/bin/helm upgrade qdrant qdrant/qdrant \
--namespace ai \
--values /home/ktayl/minicloud-ktaylorganisation/ansible/helm-values/qdrant-values.yaml \
--wait"
# 5. Scale back up and verify
kubectl scale deployment qdrant -n ai --replicas=1
kubectl rollout status deployment/qdrant -n ai
# 6. Verify collection counts match pre-migration
kubectl exec -n ai deployment/qdrant -- \
curl -s http://localhost:6333/collections | jq '.result.collections[].name'
Fine-Tuning Gateβ
If fine-tuning of llama3.2 on internal HDI Seguros documents is ever considered, the following safety checklist must be completed and documented in a closed GitHub issue before the fine-tuning workflow can run:
| Item | Why |
|---|---|
| Differential privacy training (DP-SGD, Ξ΅ β€ 8) | Limits how much any single training example can influence model weights β caps memorisation |
| Training data PII scan + removal (Presidio batch) | No names, IBANs, policy numbers, or national IDs in training set |
| Membership inference attack test | Verify the fine-tuned model cannot confirm whether a specific document was in training data |
| Model inversion attack test (Carlini et al. protocol) | Verify the model does not reproduce verbatim training text under systematic prompting |
| Output filter blocking verbatim training data reproduction | Last-resort post-call filter if the above controls are insufficient |
| Legal review | Fine-tuning on policyholder documents may create GDPR Art. 22 obligations or IP issues |
A GitHub Actions workflow (finetuning-gate.yml) enforces this β the fine-tuning run fails unless the checklist issue is closed. This gate cannot be bypassed without modifying the workflow, which itself requires a PR review.
Current status: fine-tuning is not planned. Base models (llama3.2:1b/3b) are used as-is. The gate exists to prevent the checklist being skipped when fine-tuning is eventually evaluated.
Grafana Alertingβ
| Alert | Condition | Severity |
|---|---|---|
KnowledgeBaseEnumerationDetected | API key reaches 80 unique topics / hour | Critical, fires immediately |
EnumerationWarning | API key reaches 60 unique topics / hour | Warning, fires immediately |
VerbatimReproductionSpike | > 15% of responses have abstraction_compliance = 0 over 1 hour | Warning, fires after 15 min |
Operational Checksβ
# Check enumeration counters in Redis (any key near threshold)
kubectl exec -n ai deployment/redis -- \
redis-cli KEYS "enum_topics:*" | xargs -I{} sh -c \
'echo -n "{} topics: "; redis-cli PFCOUNT {}'
# Check verbatim reproduction rate (last 24h)
# β Langfuse β Scores β abstraction_compliance β filter value = 0
# Check enumeration events today
# β Langfuse β Events β filter: name = "enumeration-detected" OR "enumeration-warning"
# Verify Qdrant PVC is on encrypted StorageClass
kubectl get pvc -n ai -o custom-columns=\
'NAME:.metadata.name,STORAGECLASS:.spec.storageClassName,STATUS:.status.phase'
# Confirm LUKS device active on Qdrant node
kubectl get pod -n ai -l app=qdrant -o wide # find node
ssh <qdrant-node> "lsblk -o NAME,TYPE,FSTYPE | grep -i luks"
What These Controls Do Not Coverβ
| Gap | Reason | Path |
|---|---|---|
| Adversarial embedding queries (Vec2Text-style attack on live Qdrant API) | Qdrant API is internal-only; requires cluster access first | Covered by Tailscale + Cilium NetworkPolicy |
| Enumeration below 80 topics/hour | A patient attacker queries 79 topics/hour indefinitely | Longer-window detection (24h sliding window) as v2 enhancement |
| Fine-tuned model memorisation | Not applicable today β no fine-tuning planned | Fine-tuning gate blocks this path until safety checklist is complete |
| Model extraction / surrogate model training | Requires thousands of API calls with outputs; rate limits slow this significantly | Existing per-key rate limits from issue #34 |