Une architecture RAG sécurisée tient sur trois piliers indissociables : segmentation (qui peut voir quoi), contrôle d'accès (sous quelles conditions), traçabilité (qui a fait quoi quand). Faiblir sur un seul casse les deux autres : sans segmentation, l'access control est un théâtre ; sans access control, la segmentation est inopérante ; sans traçabilité, les deux premiers sont indéfendables en audit. Cet article documente le blueprint d'architecture pour ces trois piliers, avec patterns concrets, code, mappings conformité (NIST, ISO 27001, EU AI Act, RGPD).
Pour le contexte risques : sécuriser un système RAG. Pour l'exfiltration côté output : empêcher l'exfiltration via chatbot RAG. Pour le poisoning : RAG poisoning.
Le triptyque non-négociable
| Pilier | Question répondue | Conséquence si absent |
|---|---|---|
| Segmentation | Qui est dans quel périmètre ? | Cross-tenant leak, KB extraction |
| Contrôle d'accès | Sous quelles conditions cette donnée est lisible ? | Privilege escalation, ACL violations |
| Traçabilité | Qui a fait quoi, quand, pourquoi ? | Forensique impossible, conformité KO |
Les trois sont indépendants mais leur défaillance se compose : un bug de segmentation devient un incident grave seulement parce qu'il n'a pas été détecté par la traçabilité, et n'a pas été contenu par l'access control. La défense en profondeur RAG passe par la robustesse simultanée des trois.
Info — Mapping : OWASP LLM02 (disclosure), LLM06 (excessive agency), LLM08 (vector/embedding). NIST AI RMF : Govern + Map + Manage. ISO 27001 : A.5 (politique), A.8 (asset management), A.9 (access control), A.12 (operations security), A.16 (incident management). Cf. audit conformité NIST/ISO/EU AI Act.
Pilier 1 — Segmentation
Trois axes à combiner. La segmentation efficace est multi-axes ; un seul axe ne suffit jamais.
Axe 1.1 — Tenant
L'isolation entre tenants distincts est la frontière la plus critique. Un tenant ne doit jamais voir les données d'un autre, indépendamment de toute permission spécifique.
| Niveau d'isolation | Implémentation | Cas d'usage |
|---|---|---|
| Index dédié | Un index/collection par tenant | Critique : santé, finance, défense |
| Namespace strict | Un namespace par tenant dans index partagé | SaaS multi-tenant grand public |
| Metadata filter | filter: {tenant_id: ...} sur recherche | Insuffisant seul — toujours combiner |
# Pattern d'isolation tenant strict (Pinecone-like)
class TenantScopedRAG:
def __init__(self, vector_db, tenant_id: str):
self.tenant_id = tenant_id
# Connexion préfixée — impossible d'oublier le tenant
self.namespace = f"tenant_{tenant_id}"
def query(self, query_text: str, user_context: dict, top_k: int = 5):
# Le namespace est injecté côté API. Pas de override possible côté client.
return self.vector_db.query(
namespace=self.namespace,
vector=embed(query_text),
filter=self._build_user_filter(user_context),
top_k=top_k,
)
def _build_user_filter(self, ctx: dict) -> dict:
# ACL filter additionnel au-dessus de la segmentation tenant
return {
"$or": [
{"acl_public": True},
{"acl_users": {"$in": [ctx["user_id"]]}},
{"acl_groups": {"$in": ctx["groups"]}},
]
}Axe 1.2 — Sensibilité
Chaque chunk porte un niveau de sensibilité (typique : public, interne, confidentiel, restreint). Le retrieval filtre selon les droits du user.
# Politique de sensibilité (exemple)
sensitivity_levels:
- name: public
score: 0
description: "Doc accessible à tous"
- name: internal
score: 10
description: "Doc interne entreprise"
- name: confidential
score: 50
description: "Doc confidentiel — RH, Finance"
- name: restricted
score: 90
description: "Doc restreint — direction, audit"
user_clearance:
default: 10 # internal
hr_admin: 50
cfo: 90
external: 0Filter de retrieval :
def get_user_clearance(user) -> int:
return max([CLEARANCE_BY_ROLE.get(r, 0) for r in user.roles] + [0])
results = vdb.query(
namespace=tenant_namespace,
vector=embedding,
filter={
"sensitivity_score": {"$lte": get_user_clearance(current_user)},
# ... autres ACLs
},
top_k=10,
)Axe 1.3 — Domaine / unité d'affaires
Pour les organisations larges, ségrégation par fonction (RH, Finance, Tech, Legal). Chaque domaine a ses propres documents, ses propres ACL.
# Exemple : domaine encodé en metadata
chunk_metadata = {
"tenant_id": "company_a",
"domain": "hr", # finance, tech, legal, sales, etc.
"sensitivity": "internal",
"owner_user_id": "u_42",
"owner_groups": ["hr_team"],
# ...
}
# Filter retrieval domaine + sensibilité + ACL
filter_query = {
"domain": {"$in": current_user.allowed_domains},
"sensitivity_score": {"$lte": current_user.clearance},
# ... acl
}Pour les très grandes organisations : ajouter un 4ème axe géographique (region: ["EU", "US", "APAC"]) pour la data residency (RGPD, Schrems II, etc.).
Patterns de segmentation cumulés
def build_retrieval_filter(user, query_context):
"""Construit un filter retrieval combinant tous les axes."""
return {
"tenant_id": user.tenant_id, # axe 1
"sensitivity_score": {"$lte": user.clearance_score}, # axe 2
"domain": {"$in": user.allowed_domains}, # axe 3
"region": {"$in": user.allowed_regions}, # axe 4 si applicable
"$or": [ # ACL fine
{"acl_public": True},
{"acl_users": {"$in": [user.id]}},
{"acl_groups": {"$in": user.groups}},
],
}Pilier 2 — Contrôle d'accès
2.1 — RBAC vs ABAC
| Critère | RBAC | ABAC |
|---|---|---|
| Décision basée sur | Rôle système | Attributs (user, resource, env) |
| Expressivité | Simple, grossier | Expressif, contextuel |
| Multi-tenant naturel | Non | Oui |
| Conditions complexes | Difficile (rôles explosent) | Naturel (policies) |
| Outils | LDAP, Spring Security, RBAC native frameworks | OpenFGA, AWS Cedar, Permit.io, Casbin |
| Maintenance | Simple à petite échelle | Plus structuré à l'échelle |
Recommandation : ABAC dès qu'on dépasse le RAG mono-équipe ou qu'on a multi-tenant. RBAC seul = ingérable rapidement.
# Exemple Cedar-like policy
permit (
principal in Group::"hr_team",
action == Action::"read_document",
resource
)
when {
resource.tenant_id == principal.tenant_id &&
resource.domain == "hr" &&
resource.sensitivity_score <= principal.clearance_score &&
(resource.acl_public ||
principal in resource.acl_groups ||
principal == resource.owner)
};2.2 — ACL propagation
Les documents source ont des ACL natives (Sharepoint groups, Drive permissions, Confluence spaces, GitHub teams). Ces ACL doivent être dénormalisées en metadata sur chaque chunk indexé.
def index_document_with_acl(doc, vector_db):
"""Indexe un document en propageant ses ACL source au niveau chunk."""
source_acl = get_native_acl(doc.source_id, doc.source_type)
chunks = chunk_document(doc.content)
for chunk in chunks:
embedding = embed(chunk.text)
vector_db.upsert(
id=f"{doc.id}_chunk_{chunk.idx}",
vector=embedding,
namespace=f"tenant_{doc.tenant_id}",
metadata={
"doc_id": doc.id,
"tenant_id": doc.tenant_id,
"domain": doc.domain,
"sensitivity": doc.sensitivity,
"sensitivity_score": SENSITIVITY_SCORES[doc.sensitivity],
"acl_public": source_acl.is_public,
"acl_users": source_acl.users,
"acl_groups": source_acl.groups,
"owner": source_acl.owner,
"indexed_at": datetime.utcnow().isoformat(),
"acl_version": source_acl.version, # pour invalidation
},
)2.3 — Pre-filter et post-filter
| Étape | Rôle | Outil |
|---|---|---|
| Pre-filter | Au niveau VDB, élimine les candidats non autorisés avant similarity | Metadata filter natif |
| Similarity search | Sur le sous-ensemble pré-filtré | Algorithme HNSW/IVF |
| Post-filter | Re-vérifie l'ACL contre l'identité courante | Service IAM source-of-truth |
def secure_retrieve(query: str, user) -> list[Chunk]:
# 1. Pre-filter au niveau VDB
candidates = vector_db.query(
namespace=f"tenant_{user.tenant_id}",
vector=embed(query),
filter=build_retrieval_filter(user),
top_k=20, # un peu plus large pour absorber le post-filter
)
# 2. Post-filter contre IAM source-of-truth
verified = []
for c in candidates:
if iam_service.can_access(
user=user,
resource_id=c.metadata["doc_id"],
action="read",
):
verified.append(c)
else:
log_event("acl_drift_detected", doc_id=c.metadata["doc_id"], user=user.id)
return verified[:5] # top-5 après post-filterPour le détail privilege escalation : privilege escalation agents IA.
2.4 — Just-in-time access via tokens
Pour les opérations sensibles, downscoper les credentials par requête (pattern ephemeral credentials) :
def fetch_chunk_content(chunk_id: str, user) -> str:
# Émettre un token éphémère scopé à ce chunk + cet utilisateur
eph_token = iam_service.issue_ephemeral_token(
subject=user.id,
resource=chunk_id,
action="read",
ttl_seconds=30,
)
return content_store.fetch(chunk_id, auth_token=eph_token)Pilier 3 — Traçabilité
3.1 — Logs structurés OpenTelemetry GenAI
Format standard depuis 2024 (semantic conventions 1.27+). Tous les outils d'observabilité modernes (Langfuse, LangSmith, Phoenix Arize) le supportent.
{
"timestamp": "2026-04-30T11:23:45.123Z",
"request_id": "req_8a92c4d1",
"session_id": "sess_91fe...",
"user": {
"id": "u_42",
"tenant_id": "company_a",
"ip": "203.0.113.10",
"auth_method": "oidc"
},
"rag": {
"query_text_hash": "sha256:abc...",
"embedding_model": "text-embedding-3-large",
"retrieved_chunks": [
{"id": "doc1_chunk_3", "score": 0.89, "sensitivity": "internal"},
{"id": "doc2_chunk_1", "score": 0.85, "sensitivity": "internal"}
],
"filter_applied": {
"tenant_id": "company_a",
"sensitivity_max": 10,
"domains": ["hr"]
}
},
"llm": {
"model": "claude-sonnet-4-6",
"input_tokens": 1234,
"output_tokens": 287,
"response_hash": "sha256:def..."
},
"security": {
"input_score": 0.05,
"output_score": 0.0,
"canary_leak": false,
"action_taken": "allowed"
},
"duration_ms": 412
}Émis vers OTLP collector → SIEM (Splunk, Sentinel, Elastic).
3.2 — Niveaux de log
Trois niveaux pour gérer le volume vs la complétude :
| Niveau | Contenu | Rétention typique |
|---|---|---|
| Hash-only (default) | Hashes des prompts/réponses, métadonnées | 90-365 jours (compliance) |
| Sampled clear | 1% des requêtes en clair (audit qualité) | 30-90 jours |
| Alert clear | Réponses des incidents en clair (forensique) | 30-90 jours sur signal |
Le niveau "sampled clear" est crucial pour pouvoir reconstituer un incident a posteriori sans tout conserver en clair (RGPD).
3.3 — Conformité RGPD
Quatre obligations à intégrer :
- Article 30 — registre des traitements : documenter le RAG comme traitement, finalité, base légale, destinataires.
- Articles 13-14 — information : informer les utilisateurs (CGU, popup) que leurs requêtes sont loggées.
- Article 17 — droit à l'effacement : capacité à purger les logs d'un user sur demande.
- Article 32 — sécurité : logs eux-mêmes protégés (chiffrement, ACL strict, audit d'accès).
DPIA (Data Protection Impact Assessment) souvent obligatoire pour les RAG manipulant des données personnelles à grande échelle.
3.4 — Audit trail par chunk
Au-delà des logs runtime, conserver un audit trail au niveau chunk :
chunk_audit_trail = {
"chunk_id": "doc1_chunk_3",
"events": [
{"type": "indexed", "doc_id": "doc1", "by": "ingestion_pipeline", "at": "2026-04-15T..."},
{"type": "acl_updated", "by": "iam_sync", "at": "2026-04-20T..."},
{"type": "retrieved", "session_id": "sess_91fe", "user": "u_42", "at": "2026-04-30T..."},
{"type": "flagged", "reason": "outlier_query_match", "at": "2026-04-29T..."},
]
}Permet de reconstituer toute interaction avec un chunk en cas d'incident.
Architecture de référence
┌─────────────────────────────────────────────────────────────┐
│ Authentification (OIDC / SAML) │
│ + Service IAM (OpenFGA / Cedar) │
└─────────────────────────────────┬───────────────────────────┘
│ user context
┌─────────────────────────────────▼───────────────────────────┐
│ API Gateway │
│ - Rate limiting │
│ - Detection extraction (volume, diversité) │
│ - Logging structuré OTel │
└─────────────────────────────────┬───────────────────────────┘
│
┌─────────────────────────────────▼───────────────────────────┐
│ RAG Orchestrator │
│ - Build filter (tenant + sensibilité + domaine + ACL) │
│ - Pre-filter VDB │
│ - Similarity search │
│ - Post-filter via IAM (re-vérification) │
│ - Sanitization chunks │
└──────────┬──────────────────────────────────┬──────────────┘
│ │
┌──────────▼─────────┐ ┌─────────▼─────────────┐
│ Vector DB │ │ IAM service │
│ - Index par tenant │ │ - ABAC policies │
│ - Encrypt at-rest │ │ - Ephemeral tokens │
│ - Audit logs natif │ │ - Audit logs │
└────────────────────┘ └───────────────────────┘
│ │
┌──────────▼──────────────────────────────────▼─────────────┐
│ LLM avec system prompt durci │
│ + Output filter (DLP, canary, allowlist domaines) │
└──────────┬────────────────────────────────────────────────┘
│
┌──────────▼────────────────────────────────────────────────┐
│ UI sanitization (DOMPurify, CSP, no markdown image) │
└──────────┬────────────────────────────────────────────────┘
│
┌──────────▼────────────────────────────────────────────────┐
│ Telemetry → OTel collector → SIEM (Splunk/Sentinel/Elastic)│
└────────────────────────────────────────────────────────────┘Chaque composant émet ses propres logs structurés vers le collector OTel central. Le SIEM corrèle.
Mapping conformité
NIST AI RMF
| Fonction NIST | Application RAG |
|---|---|
| Govern | Politique RAG documentée, RACI, cadence audit |
| Map | Threat model RAG (3 classes risques + cycle vie) |
| Measure | Métriques par pilier (FPR isolation, ACL drift, log completeness) |
| Manage | Runbooks SOC, calibration, gestion incident |
EU AI Act
Pour systèmes RAG considérés "à haut risque" (santé, RH, justice, etc.) :
- Article 9 — gestion des risques (threat model)
- Article 10 — gouvernance des données (segmentation, qualité)
- Article 12 — record-keeping (traçabilité native)
- Article 13 — transparence (information user)
- Article 14 — surveillance humaine (HITL)
- Article 15 — cybersécurité (les 3 piliers)
ISO 27001 (Annexe A)
| Contrôle | Lien RAG |
|---|---|
| A.5 — Politique de sécurité | Documentation des 3 piliers |
| A.8 — Gestion des actifs | Inventaire corpus + classification sensibilité |
| A.9 — Contrôle d'accès | Pilier 2 (RBAC/ABAC, ACL propagation) |
| A.12 — Sécurité des opérations | Pilier 3 (logs, monitoring) |
| A.16 — Gestion des incidents | Runbook SOC RAG |
| A.18 — Conformité | RGPD + EU AI Act + sectorielle |
ISO 42001 (AI management system)
Standard plus récent (décembre 2023) qui couvre spécifiquement le management de l'IA. Les 3 piliers s'alignent naturellement.
Pour le détail conformité : audit conformité NIST/ISO/EU AI Act.
Tests d'audit par pilier
Audit segmentation
def audit_segmentation(rag_app):
issues = []
# Test cross-tenant
canary = "TENANT_A_CANARY_3f4d92"
rag_app.index_for_tenant("tenant_a", document=canary)
for resp in rag_app.query_as_tenant("tenant_b", queries=SAMPLE_QUERIES):
if canary in resp:
issues.append({"type": "cross_tenant_leak"})
# Test sensibilité
classified_doc = "RESTRICTED-INFO-ONLY-LEVEL-90"
rag_app.index_doc(classified_doc, sensitivity_score=90)
for resp in rag_app.query_as_user(user_clearance=10, queries=...):
if classified_doc in resp:
issues.append({"type": "sensitivity_leak"})
return issuesAudit contrôle d'accès
def audit_access_control(rag_app, iam):
# Test ACL propagation : créer doc avec ACL stricte, requérir depuis user sans droit
doc_id = rag_app.index_doc(content="...", acl_users=["u_42"])
resp = rag_app.query_as_user("u_other", "give me content of " + doc_id)
assert doc_id not in resp
# Test ACL drift : doc avec ACL périmée
iam.revoke(user="u_42", doc=doc_id)
resp = rag_app.query_as_user("u_42", ...)
assert "content of doc" not in resp # post-filter doit attraperAudit traçabilité
def audit_traceability(rag_app, siem):
# Tester complétude des logs
rag_app.query_as_user("u_42", "test query")
logs = siem.fetch_recent(user="u_42", limit=1)
assert logs[0].user.id == "u_42"
assert logs[0].rag.retrieved_chunks # liste présente
assert logs[0].llm.model # info LLM présente
assert logs[0].security.action_taken
# Tester reproductibilité d'un incident
incident_id = "inc_test_001"
rag_app.simulate_incident(incident_id)
reconstructed = siem.reconstruct_session(incident_id)
assert reconstructed.is_completePoints clés à retenir
- Architecture RAG sécurisée = 3 piliers indissociables : segmentation, contrôle d'accès, traçabilité.
- Segmentation : tenant (toujours strict, idéalement index dédié pour critique), sensibilité (4 niveaux), domaine, optionnellement résidence géographique.
- Contrôle d'accès : ABAC obligatoire dès multi-tenant (OpenFGA, Cedar, Permit.io). ACL propagées de la source au chunk indexé. Pre-filter VDB + post-filter IAM systématiquement.
- Traçabilité : OpenTelemetry GenAI semantic conventions (1.27+). Logs structurés, 3 niveaux (hash, sampled clear, alert clear) pour balancer volume vs complétude vs RGPD.
- Conformité : RGPD (Art. 13/14/17/30/32 + DPIA), EU AI Act (Art. 9/10/12/13/14/15 pour high-risk), NIST AI RMF, ISO 27001 (A.5, A.8, A.9, A.12, A.16, A.18), ISO 42001.
- Architecture de référence en 7 étages : auth → API gateway → orchestrator → VDB + IAM → LLM → output filter → UI → telemetry.
- Audit par pilier : segmentation (canary cross-tenant), access control (ACL propagation + drift), traçabilité (complétude + reproductibilité incident).
- Faiblir sur un pilier ruine les deux autres — la défense est simultanée, pas séquentielle.
Une architecture RAG sécurisée n'est pas un produit qu'on installe. C'est un blueprint qu'on assemble à partir de composants matures (OpenFGA pour ABAC, Pinecone/Weaviate pour la VDB, OTel pour la trace, Presidio pour DLP, Langfuse pour l'observabilité). Le travail d'architecte est dans l'intégration cohérente des trois piliers, pas dans la création de chaque pilier ex nihilo.







