Field Log · Entry

RAG ACL·Multi-tenancy·Security Filtering: 검색 전에 권한을 지키는 법 (5/10)

사용자 인증 정보와 최신 권한 snapshot이 tenant·ACL pre-filter를 거쳐 허용된 chunk만 reranker와 LLM, citation 단계로 전달되는 Enterprise RAG 보안 경계

오늘의 결론

  • Enterprise RAG의 핵심 보안 조건은 간단합니다. 허용되지 않은 chunk는 점수가 높아도 retrieval 경계를 넘어오면 안 됩니다.
  • Authentication은 호출자가 누구인지 확인하고, authorization은 그 호출자가 지금 이 문서를 읽을 수 있는지 결정합니다. API key 하나로 둘을 대신하지 않습니다.
  • Tenant·ACL 조건은 top-k를 뽑은 뒤 지우는 장식이 아닙니다. 가능한 한 ANN 후보 생성 안쪽에서 적용하고, 불가능하면 별도 허용 집합과 과소 검색 위험을 명시합니다.
  • 권한은 index를 만들 때 한 번 복사하고 끝나는 metadata가 아닙니다. Group 변경, 상위 폴더 상속, 문서 공유 해제와 긴급 차단을 content보다 더 짧은 revoke SLO로 동기화합니다.
  • Retrieval만 안전해도 충분하지 않습니다. Query cache, reranker, LLM context, citation fetch, agent tool, trace와 feedback 저장소까지 같은 authorization context를 유지해야 합니다.

앞 글에서는 metadata를 typed assertion과 canonical entity로 만들었습니다. 그중 tenant_id, acl_ref, classification은 검색 품질용 facet이 아니라 보안 정책의 입력입니다. 이번 글은 이 값을 어떻게 실제 접근 제어로 연결하는지 설명합니다.

authenticated principal + tenant + current group membership
  → policy decision
  → tenant and ACL constrained candidate generation
  → authorized reranking
  → authorized context assembly
  → authorized citation and tool fetch
  → audit without secret leakage

인증된 principal과 최신 ACL을 query-time filtered retrieval부터 reranker, LLM, citation까지 유지하고 revoke event와 emergency deny를 빠르게 반영하는 RAG authorization 구조

먼저 답하기: ACL을 vector 검색 뒤에 적용하면 왜 위험한가?

검색기가 tenant를 무시하고 전체 index에서 10개를 찾은 뒤 애플리케이션이 9개를 지운다고 가정해 봅시다.

  1. 사용자는 결과를 1개만 받으므로 실제 관련 문서를 놓칩니다.
  2. 지우기 전 candidate가 reranker, trace, cache나 debug log에 들어갈 수 있습니다.
  3. Unauthorized 문서의 score·title·존재 여부가 side channel이 될 수 있습니다.
  4. Top-k를 크게 늘리는 oversampling도 허용 문서 recall을 보장하지 않습니다.

따라서 authorization은 결과 꾸미기가 아니라 candidate set을 정의하는 검색 의미의 일부입니다.

이 글의 대상과 학습 시간

  • 대상: 사내 문서 검색, 고객별 SaaS RAG, 지원 상담 Agent를 설계하는 초·중급 개발자
  • 선수 지식: tenant, group, vector search와 metadata filter의 기본 개념
  • 빠르게 읽기: 약 12분
  • 설계까지 이해하기: 약 35분
  • threat test와 revoke drill 포함: 약 90분

이 글은 특정 vendor의 설정법보다 재사용 가능한 authorization invariant에 집중합니다. 규제·계약·노무 정책에 대한 법률 자문은 아니며, 실제 정책은 보안·법무·data owner와 합의해야 합니다.

1. 먼저 보호 대상을 적는다

보안 설계는 “ACL field를 넣자”에서 시작하지 않습니다. 무엇이 노출되면 안 되는지 자산과 경로를 적습니다.

자산노출 예필요한 경계
원문기밀 보고서 본문source authorization
Chunk원문의 일부 문장·표retrieval authorization
Metadata제목, author, 장비명field-level redaction 여부
Embedding역추론·membership inference 가능성tenant·storage boundary
Search score문서 존재 여부 추정response·trace 최소화
Citationsigned URL, file pathclick-time 재검증
Prompt·answer허용 문서가 섞인 contextsession·feedback 정책
Cache다른 사용자의 결과auth-aware key와 TTL
Tracequery, chunk, policy decision접근 통제·redaction

Embedding이 원문처럼 바로 읽히지 않는다고 해서 공개 데이터가 되는 것은 아닙니다. Index snapshot, backup, offline evaluation export도 같은 데이터 분류를 적용합니다.

2. Authentication과 Authorization을 분리한다

두 용어를 섞으면 설계가 빠르게 무너집니다.

Authentication: 누구인가?

예를 들면 다음을 확인합니다.

  • user 또는 workload identity
  • token issuer와 audience
  • token 만료와 서명
  • tenant membership
  • MFA·device posture 같은 조건부 신호

Authorization: 지금 무엇을 할 수 있는가?

같은 사용자도 action과 resource에 따라 결과가 달라집니다.

can principal:u42 perform read
on document:doc-73
inside tenant:t9
at policy revision:p184?

search index에 접속 가능index 안 모든 문서를 읽을 수 있음도 다른 권한입니다. Search service credential은 애플리케이션의 infrastructure access를 증명할 뿐, 최종 사용자의 document access를 자동으로 증명하지 않습니다.

Microsoft의 Azure AI Search 문서도 service-level 역할과 document-level access를 구분하며, security filter 방식은 query에 user·group identity를 넣어 일치하지 않는 문서를 제외하는 패턴이라고 설명합니다. Native ACL 계열 기능은 2026년 7월 문서 기준 preview 범위가 있으므로 API version과 제약을 별도로 확인해야 합니다.

3. Authorization invariant를 한 문장으로 고정한다

설계 리뷰와 테스트가 공유할 문장을 먼저 만듭니다.

어떤 query, retry, cache hit, fallback, citation click에서도 현재 authorization context가 허용하지 않은 resource의 content·metadata·existence signal이 사용자나 downstream model에 전달되지 않는다.

이를 단계별 불변식으로 풀면 다음과 같습니다.

candidate_chunks ⊆ resources_allowed(principal, tenant, action, policy_revision)
reranked_chunks ⊆ candidate_chunks
context_chunks ⊆ reranked_chunks
citation_targets ⊆ resources_allowed(now)

마지막 now가 중요합니다. Answer를 만든 시점에는 허용됐지만 사용자가 citation을 클릭할 때 권한이 회수됐을 수 있기 때문입니다.

4. Tenant boundary부터 선택한다

Multi-tenancy는 ACL과 같지 않습니다. Tenant는 가장 굵은 격리 경계이고 ACL은 그 안의 세부 접근 규칙입니다.

패턴 A. Shared index + tenant_id 필터

one index
  tenant_id = t1 | t2 | t3
  ACL fields per document/chunk

장점:

  • 운영 단위가 적다.
  • 작은 tenant가 많을 때 자원 효율이 좋다.
  • 공통 schema와 migration을 적용하기 쉽다.

위험:

  • 필터 누락 한 번이 cross-tenant leak으로 이어진다.
  • cache key, admin query, debug tool도 tenant를 강제해야 한다.
  • 한 tenant의 부하가 다른 tenant latency에 영향을 줄 수 있다.

패턴 B. Index 또는 collection per tenant

장점:

  • 데이터 경계가 명확하다.
  • tenant별 삭제, backup, encryption과 용량 관리가 쉽다.
  • query에서 tenant filter를 빠뜨려도 blast radius가 작다.

위험:

  • tenant가 많으면 index 수와 migration 비용이 커진다.
  • 작은 index는 ANN 효율과 resource utilization이 나쁠 수 있다.
  • schema drift가 생기기 쉽다.

패턴 C. Cluster·account per security domain

규제, 고객 계약, 지역 경계가 강하면 infrastructure 자체를 나눕니다. 비용은 크지만 key, network, operator와 backup 경계까지 분리할 수 있습니다.

선택 원칙

질문Shared index에 유리물리 분리에 유리
Tenant 수매우 많음적고 큼
Cross-tenant 위험 허용도낮아도 강한 guard 가능거의 0에 가까움
고객별 schema비슷함크게 다름
삭제·backup 요구공통tenant별 엄격
장애 격리논리 격리 가능독립 격리 필요
운영 인력제한적전담 가능

하나만 선택할 필요는 없습니다. 작은 tenant는 shared pool, 민감 tenant는 dedicated deployment로 나누는 tiered model도 가능합니다. 단, routing rule과 migration 절차가 보안 정책의 일부가 됩니다.

5. Principal과 group을 stable ID로 표현한다

ACL에 email이나 표시 이름을 그대로 넣지 않습니다.

{
  "principal_id": "usr_8f4a",
  "tenant_id": "tenant_fab_a",
  "group_ids": ["grp_process", "grp_etch_owner"],
  "auth_time": "2026-07-16T01:10:00Z",
  "membership_revision": "m-20491",
  "policy_revision": "p-184"
}

필요한 성질은 다음과 같습니다.

  • ID는 이름 변경과 재사용에 흔들리지 않는다.
  • Tenant scope가 명시된다.
  • Nested group을 누가, 언제, 어느 깊이까지 확장하는지 정한다.
  • Disabled user와 탈퇴 tenant를 즉시 거부한다.
  • Session token에 담긴 group이 오래됐을 때의 정책을 정한다.

Group을 쓰면 관리가 쉬워지지만 revocation path가 길어집니다. User 탈퇴 → identity provider membership 변경 → policy store 반영 → search ACL projection 갱신 → cache 만료까지 각 지연을 측정해야 합니다.

6. ACL model을 source semantics에 맞춘다

ACL을 무조건 allowed_group_ids 한 필드로 평탄화하면 다음 의미를 잃을 수 있습니다.

  • explicit allow
  • explicit deny
  • parent folder에서 상속
  • inheritance 중단
  • owner·editor·viewer action 차이
  • sensitivity label과 별도 policy
  • 시간 제한 공유
  • legal hold·incident deny

Canonical permission snapshot은 source 의미를 보존하고, 검색 index에는 안전하게 평가 가능한 projection을 둡니다.

{
  "resource_id": "doc_0194",
  "tenant_id": "tenant_fab_a",
  "source_acl_revision": "sp-etag-7781",
  "captured_at": "2026-07-16T01:08:12Z",
  "inheritance": {
    "mode": "inherited",
    "parent_resource_id": "folder_etch_ops"
  },
  "entries": [
    {"effect": "allow", "subject": "grp_etch_owner", "action": "read"},
    {"effect": "deny", "subject": "usr_external_19", "action": "read"}
  ],
  "classification": "internal-restricted",
  "policy_revision": "p-184"
}

Allow와 deny의 충돌

조직마다 precedence가 다릅니다. 흔한 보수적 예는 다음과 같습니다.

tenant mismatch → deny
explicit emergency deny → deny
source explicit deny → deny
matching explicit/inherited allow → allow
otherwise → deny

이 순서를 암묵적으로 구현하지 말고 versioned policy로 둡니다. deny by default가 기본입니다.

Action을 분리한다

read만 있어 보여도 실제 RAG에는 여러 action이 있습니다.

  • discover: 제목이나 존재를 볼 수 있음
  • read: chunk 본문을 검색 context로 사용
  • cite: 원문 링크와 위치를 반환
  • summarize: model 처리 허용
  • export: 결과 다운로드
  • administer: ACL·index 상태 조회

어떤 문서는 사람이 읽을 수 있어도 외부 model로 전송할 수 없을 수 있습니다. 이때 readprocess_with:model_class를 분리합니다.

7. Document ACL을 chunk에 안전하게 투영한다

RAG index의 검색 단위는 대개 chunk입니다. Document에만 ACL이 있고 chunk row에 없다면 filtered ANN을 적용하기 어렵습니다.

source document ACL snapshot
  → canonical resource permission
  → section override resolution
  → chunk ACL projection
  → index generation

Chunk에 최소한 다음 값을 둡니다.

{
  "chunk_id": "chk_doc0194_p7_03",
  "document_id": "doc_0194",
  "tenant_id": "tenant_fab_a",
  "acl_set_id": "acl_55bc",
  "allowed_subject_ids": ["grp_etch_owner"],
  "denied_subject_ids": ["usr_external_19"],
  "policy_revision": "p-184",
  "acl_captured_at": "2026-07-16T01:08:12Z"
}

Large group list를 모든 chunk에 복제하면 index가 커집니다. 대안은 다음과 같습니다.

  1. 작은 ACL은 subject ID를 직접 projection합니다.
  2. 큰 ACL은 acl_set_id로 deduplicate하고 engine이 허용 집합 join을 지원하는지 검토합니다.
  3. Tenant·classification처럼 selectivity가 큰 field를 먼저 filter합니다.
  4. Search engine이 표현하지 못하는 복잡 정책은 coarse pre-filter와 authoritative policy check를 조합합니다.

단, coarse filter가 unauthorized candidate를 downstream에 보낸다면 안전하지 않습니다. 정책 확인 전에는 content를 reranker나 LLM에 전달하지 않습니다.

8. Pre-filter, in-search filter, post-filter를 구분한다

Vector engine의 filter라는 이름만 보고 동일한 보안 성질을 기대하면 안 됩니다.

Pre-filter 또는 efficient in-search filtering

필터를 만족하는 문서 공간 안에서 ANN candidate를 찾습니다.

allowed set ∩ vector candidate exploration → top-k

장점:

  • 허용 집합 안에서 top-k를 채우기 쉽습니다.
  • Unauthorized candidate가 후단으로 넘어갈 가능성을 줄입니다.
  • Restrictive filter에서 recall을 이해하기 쉽습니다.

Post-filter

전체 vector top-k를 먼저 얻고 결과에 filter를 적용합니다.

global top-k → discard unauthorized → maybe fewer than k

보안 경계 전용으로 쓰기 어렵고, 허용 결과 recall도 나쁠 수 있습니다.

OpenSearch 공식 문서는 vector filter 방식별로 scoring script·boolean post-filter·efficient filtering을 구분하고, post-filter는 요청한 k보다 적은 결과를 반환할 수 있다고 설명합니다. 실제 engine, index type과 search mode가 필터를 어디에서 적용하는지 확인해야 합니다.

Oversampling은 보안 증명이 아니다

k=10 대신 1,000개를 가져와 ACL로 지우면 결과 수는 늘 수 있습니다. 그러나 다음은 해결하지 못합니다.

  • 허용 문서가 global top-1,000 밖에 있을 가능성
  • 1,000개 unauthorized payload의 trace·cache 노출
  • 큰 candidate set으로 인한 latency와 비용
  • attacker가 결과 수·시간으로 corpus 분포를 추정하는 문제

Oversampling은 recall trade-off를 다루는 운영 기법일 뿐 authorization boundary가 아닙니다.

9. Query-time enforcement pipeline을 고정한다

안전한 경로를 단계별로 봅시다.

1단계. Token 검증

  • issuer, audience, signature, expiration 검증
  • user·workload 상태 확인
  • tenant routing 결정
  • request ID와 auth context 생성

2단계. 최신 subject context 구성

  • direct principal ID
  • effective group IDs
  • attributes와 device·network condition
  • membership·policy revision
  • emergency deny 상태

3단계. Policy decision

Policy Decision Point가 query action과 tenant를 평가하고 search filter 또는 allowed resource set을 만듭니다.

Open Policy Agent는 policy decision-making을 application logic에서 분리하는 general-purpose policy engine입니다. OPA를 반드시 써야 한다는 뜻은 아니지만, policy code·data·decision log를 versioning하는 관점은 유용합니다.

4단계. Authorized candidate generation

def retrieve(query, auth):
    assert auth.tenant_id is not None
    decision = policy.authorize_search(
        principal=auth.principal_id,
        groups=auth.group_ids,
        tenant=auth.tenant_id,
        action="rag.read",
        policy_revision=auth.policy_revision,
    )
    if not decision.allow:
        return []

    return index.hybrid_search(
        query=query,
        tenant_filter=decision.tenant_filter,
        acl_filter=decision.acl_filter,
        top_k=40,
    )

이 코드는 개념 예시입니다. 실제 filter expression에는 user input을 문자열 결합하지 말고 parameter binding·escaping을 사용합니다.

5단계. Defense-in-depth check

Candidate의 tenant, ACL revision과 policy decision을 다시 확인합니다. 이것이 pre-filter를 대체하지는 않지만 schema·routing 오류를 잡는 두 번째 방어선입니다.

6단계. Rerank와 context assembly

Authorized candidate만 reranker에 보냅니다. Context budget을 채우는 과정에서도 duplicate나 parent fetch가 새 resource를 읽는다면 다시 권한을 확인합니다.

7단계. Citation과 tool fetch

Citation URL을 그대로 노출하지 않습니다.

  • Opaque resource ID를 반환합니다.
  • Click 시 현재 권한을 다시 평가합니다.
  • Signed URL은 짧은 TTL과 좁은 scope를 사용합니다.
  • Agent가 원문 확대 조회 tool을 호출할 때도 auth context를 전달합니다.

10. Policy Enforcement Point를 모든 경로에 둔다

RAG 시스템에는 query API 외의 우회 경로가 많습니다.

경로Enforcement 질문
Search APItenant·ACL filter가 강제되는가?
Document preview원문 fetch에서 재검증하는가?
Agent tooltool에 principal context가 전달되는가?
Related documents새 candidate마다 권한을 확인하는가?
Feedback UIprompt·answer를 누가 볼 수 있는가?
Evaluation exportrestricted chunk가 test set으로 유출되지 않는가?
Admin consolesupport engineer 접근이 승인·감사되는가?
Debug endpointfilter off 옵션이 production에 없는가?

특히 internal=true, skip_acl=true 같은 debug flag가 사용자 입력으로 들어오지 않게 합니다. Service-to-service 호출도 workload identity와 least privilege가 필요합니다.

11. Cache key에 authorization context를 포함한다

다음 cache key는 위험합니다.

cache_key = hash(normalized_query)

같은 질문을 한 다른 tenant나 다른 group 사용자에게 결과가 재사용될 수 있습니다.

안전한 시작점은 다음과 같습니다.

cache_key = hash(
  tenant_id,
  normalized_query,
  principal_or_entitlement_fingerprint,
  membership_revision,
  policy_revision,
  index_generation,
  model_revision
)

개인별 key는 hit rate가 낮습니다. 권한이 동일한 entitlement set의 stable fingerprint를 사용할 수 있지만, collision·ordering·revocation을 검증해야 합니다.

Cache 종류도 나눕니다.

  • Query embedding cache: 대개 content 권한과 무관하지만 tenant별 model·logging 정책 확인
  • Retrieval result cache: ACL fingerprint 필수
  • Rerank cache: candidate ID와 ACL·index generation 포함
  • Answer cache: context IDs, authorization fingerprint, prompt·model revision 포함
  • Citation URL cache: 짧은 TTL, click-time 재검증

Emergency revoke는 관련 cache를 능동 무효화하거나 deny overlay가 cache hit 뒤에도 실행되게 해야 합니다.

12. 권한 동기화는 content ingestion보다 우선한다

Content가 10분 늦으면 답이 오래된 문제지만, revoke가 10분 늦으면 퇴사자나 외부 사용자가 계속 읽을 수 있는 문제입니다.

두 freshness SLO를 분리합니다.

content_freshness_slo: source update → searchable
permission_grant_slo: new allow → searchable
permission_revoke_slo: deny/revoke → no longer retrievable

일반적으로 revoke가 가장 엄격해야 합니다. 정확한 시간은 조직 위험에 따라 정하며 아래 숫자는 예시일 뿐입니다.

Event예시 목표실패 시 동작
Content 수정15분stale 표시
새 권한 grant15분잠시 검색 불가 허용
권한 revoke60초emergency deny 우선
Tenant disabled즉시gateway deny
Policy rollback5분이전 검증 revision 복원

Azure AI Search 공식 문서도 query-time enforcement가 index에 이미 동기화된 permission metadata를 기준으로 하며, source permission 변경은 해당 metadata가 갱신된 뒤 반영된다고 명시합니다. 일부 상속 권한 변경은 명시적 refresh가 필요할 수 있으므로 “native ACL이면 자동으로 즉시 안전”하다고 가정하면 안 됩니다.

13. ACL event pipeline을 별도로 관찰한다

identity/source ACL change
  → permission event log
  → normalize + resolve inheritance
  → canonical permission snapshot
  → chunk ACL projection update
  → search index visibility
  → cache invalidation
  → verification probe

각 event에는 다음을 둡니다.

{
  "event_id": "evt_acl_8821",
  "resource_id": "doc_0194",
  "tenant_id": "tenant_fab_a",
  "change": "revoke",
  "source_revision": "sp-etag-7782",
  "occurred_at": "2026-07-16T01:20:03Z",
  "observed_at": "2026-07-16T01:20:05Z",
  "indexed_at": null,
  "cache_invalidated_at": null
}

Content update queue와 같은 priority로 두면 대량 backfill에 revoke가 막힐 수 있습니다. Permission revoke는 별도 high-priority lane을 둡니다.

주기적 reconciliation도 필요하다

Event 누락과 connector bug를 대비해 source ACL snapshot과 canonical/index projection을 주기적으로 비교합니다.

  • ACL hash mismatch
  • Missing resource
  • Parent inheritance revision mismatch
  • Disabled principal 잔존
  • Index generation별 permission parity

Event-driven sync와 full reconciliation은 서로 대체 관계가 아닙니다.

14. Emergency deny overlay를 준비한다

Index update가 느리거나 source connector가 장애여도 즉시 막아야 할 때가 있습니다.

deny_overlay:
  tenant_ids
  principal_ids
  resource_ids
  classification rules
  expires_at
  incident_id

Gateway와 retrieval defense check가 이 overlay를 먼저 평가합니다. 운영자가 실수로 영구 rule을 남기지 않도록 expiry, approver와 incident ticket을 필수로 둡니다.

15. Consistency revision을 결과와 함께 추적한다

분산 authorization에서는 “어느 시점의 권한으로 결정했는가”가 중요합니다. Google Zanzibar 논문은 관계 기반 authorization을 전 세계 규모로 제공하면서 consistency와 authorization decision의 freshness를 핵심 문제로 다룹니다.

모든 시스템이 Zanzibar를 구현할 필요는 없습니다. 다만 다음 revision을 trace에 남기는 습관은 가치가 있습니다.

identity_membership_revision
policy_revision
source_acl_revision
index_generation
authorization_decision_id

User가 “방금 권한이 제거됐는데 왜 보였나”라고 물을 때 이 다섯 값이 없으면 원인을 재구성하기 어렵습니다.

16. Reranker와 LLM을 별도 trust zone으로 본다

Unauthorized candidate가 최종 answer에 안 보였다고 안전한 것이 아닙니다. 외부 API reranker나 LLM에 전송됐다면 이미 processing boundary를 넘었습니다.

확인할 항목:

  • Model provider와 data processing agreement
  • Region과 retention
  • Training opt-out 여부
  • Classification별 허용 model class
  • Prompt·response logging
  • Support access
  • Encryption와 private networking

Context builder는 classificationprocess_with policy를 평가해야 합니다.

for chunk in authorized_chunks:
    if not policy.can_process(
        principal=auth.principal_id,
        resource=chunk.document_id,
        model_class=request.model_class,
    ):
        continue
    context.add(chunk)

17. Audit log는 증거이자 민감 데이터다

좋은 authorization decision log 예시는 다음과 같습니다.

{
  "decision_id": "authz_7714",
  "request_id": "req_a912",
  "principal_hash": "hmac:...",
  "tenant_id": "tenant_fab_a",
  "action": "rag.read",
  "policy_revision": "p-184",
  "membership_revision": "m-20491",
  "index_generation": "kb-2026-07-16.3",
  "candidate_count": 40,
  "authorized_count": 40,
  "deny_reason_counts": {},
  "latency_ms": 18
}

기본 log에 query 원문, document title과 chunk text를 넣지 않습니다. 조사 시 별도 승인으로 protected trace를 열도록 합니다. Principal을 hash하더라도 재식별 위험과 key 관리가 남습니다.

18. 실패를 드러내는 지표를 만든다

Security correctness

  • Cross-tenant result count: 항상 0
  • Unauthorized candidate after enforcement: 항상 0
  • Revoked resource retrieval probe: 항상 0
  • Citation recheck failure rate
  • Cache authorization mismatch count
  • ACL projection parity rate

Freshness

  • Permission event lag p50·p95·p99
  • Revoke visibility lag p99
  • Group membership age
  • Resources with stale ACL snapshot
  • Reconciliation mismatch age

Retrieval quality under filters

  • Authorized Recall@k
  • Result fill rate
  • Filter selectivity별 latency
  • Empty result rate by tenant·group
  • Exact search와 ANN filter parity

권한을 강화한 뒤 result fill rate가 떨어지면 ACL을 완화할 것이 아니라 filtered ANN 설정, candidate exploration과 authorized corpus coverage를 점검합니다.

19. 보안 테스트는 synthetic corpus로 자동화한다

아래처럼 의도적으로 구분 가능한 secret marker를 넣은 작은 corpus를 만듭니다.

tenant A / group etch: SECRET_A_ETCH_7291
tenant A / group litho: SECRET_A_LITHO_4812
tenant B / public: SECRET_B_PUBLIC_9930
tenant B / denied user: SECRET_B_DENY_1140

Test matrix:

Case기대 결과
A 사용자가 B secret 검색0건, trace에도 payload 없음
Etch 사용자가 Litho secret 검색0건
Group 탈퇴 직후 동일 queryrevoke SLO 내 0건
Parent folder 권한 제거모든 child 0건
Cached answer 재요청권한 fingerprint mismatch로 miss
Citation click 전 revoke원문 fetch deny
Rerank fallback 발생authorized candidate만 사용
Filter service timeoutfail closed
Empty group listpublic scope 외 deny
Tenant routing 누락request 자체 deny

검색 response만 검사하지 말고 protected trace sink, cache와 model mock이 받은 payload도 검사합니다.

20. Fail-open을 제거한다

다음 fallback은 편해 보이지만 위험합니다.

try:
    groups = identity_service.get_groups(user)
except TimeoutError:
    groups = ALL_EMPLOYEES  # 금지

Authorization dependency가 실패하면 기본은 fail closed입니다.

try:
    groups = identity_service.get_groups(user)
except TimeoutError:
    raise ServiceUnavailable("authorization context unavailable")

Availability가 중요하면 다음을 조합합니다.

  • 짧은 TTL의 signed entitlement snapshot
  • 마지막 known-good revision과 최대 허용 age
  • Emergency deny overlay는 항상 최신 확인
  • Read-only degraded mode
  • Public corpus만 별도 fallback

Stale allow를 얼마나 허용할지는 risk owner가 결정합니다.

21. 가상 반도체 RAG 예시

다음은 설명을 위한 가상 상황입니다.

상황

  • Tenant: Fab-A, Fab-B
  • Fab-A group: etch_owner, vendor_maintenance
  • 문서: Etcher 장애 분석 보고서
  • Source rule: vendor는 승인 기간에 특정 appendix만 읽을 수 있음
  • 기존 구현: shared vector index에서 top-100 후 애플리케이션 ACL filter

관찰된 위험

가상의 실험에서 vendor_maintenance 허용 문서는 global top-100 밖에 있을 수 있습니다. Top-100 안의 내부 보고서를 지우면 빈 답이 나오고, debug trace에는 내부 title과 score가 남습니다.

개선 설계

  1. tenant_id=Fab-A를 mandatory route filter로 강제합니다.
  2. Approved appendix를 독립 resource·chunk로 모델링합니다.
  3. Time-bounded vendor entitlement를 policy store에서 평가합니다.
  4. ANN 검색 안쪽에서 tenant와 allowed resource filter를 적용합니다.
  5. Reranker와 LLM에는 허용 appendix chunk만 보냅니다.
  6. Citation click 때 expiry를 다시 확인합니다.
  7. 승인 종료 event를 high-priority revoke lane으로 처리합니다.

측정

  • Vendor secret leakage probe = 0
  • Revoke visibility lag p99
  • Authorized Recall@10
  • Empty result rate
  • Trace payload policy violations

숫자를 미리 정답으로 두지 않고 tenant·group·selectivity별 gold query에서 측정합니다.

22. 구현 순서

1단계. Resource와 action model

Document, section, chunk, citation, export의 identity와 action을 정의합니다.

2단계. Tenant routing 강제

Gateway에서 tenant를 token과 route로 확정하고 사용자 query parameter가 덮어쓰지 못하게 합니다.

3단계. Canonical permission snapshot

Source ACL의 allow·deny·inheritance·revision을 보존합니다.

4단계. Search projection

Engine이 안전하게 평가할 tenant·subject·classification field를 chunk에 투영합니다.

5단계. Query-time enforcement

Token→entitlement→policy decision→filtered retrieval→defense check를 하나의 library 또는 gateway로 강제합니다.

6단계. Downstream propagation

Cache, reranker, LLM, citation, tool과 trace에 auth context·decision ID를 연결합니다.

7단계. Revocation pipeline

Permission event priority, emergency deny, cache invalidation과 reconciliation을 구현합니다.

8단계. Adversarial test와 release gate

Cross-tenant·group revoke·cache·fallback·citation case가 모두 통과해야 새 index generation을 활성화합니다.

실전 체크리스트

  • 보호 자산에 원문뿐 아니라 chunk, embedding, score, citation, cache와 trace가 포함된다.
  • Authentication과 document authorization이 분리돼 있다.
  • Authorization invariant가 설계·코드·테스트에 같은 문장으로 있다.
  • Tenant boundary가 shared, dedicated 또는 hybrid 중 근거 있게 선택됐다.
  • Tenant ID는 token·routing에서 오며 user input으로 덮어쓸 수 없다.
  • Principal·group은 stable scoped ID를 쓴다.
  • Allow·deny·inheritance·action semantics가 versioned policy에 있다.
  • Unknown과 policy dependency failure는 fail closed다.
  • Source ACL revision과 capture time을 보존한다.
  • Document ACL이 section·chunk projection에 빠짐없이 전달된다.
  • Vector filter가 pre/in-search인지 post-filter인지 확인했다.
  • Oversampling을 authorization boundary로 취급하지 않는다.
  • Unauthorized candidate는 reranker와 LLM에 전달되지 않는다.
  • Parent·related document fetch도 권한을 재확인한다.
  • Citation click 시 현재 권한을 다시 확인한다.
  • Cache key에 tenant, entitlement, policy와 index revision이 있다.
  • Permission revoke가 content update보다 높은 우선순위를 가진다.
  • Revoke SLO와 emergency deny path가 있다.
  • Event 누락을 찾는 ACL reconciliation이 있다.
  • Cross-tenant·revoke·cache·fallback test가 release gate에 있다.
  • Audit log가 decision evidence를 남기되 query·content를 기본 노출하지 않는다.
  • 외부 model processing이 classification policy를 만족한다.

스스로 확인하기

Q1. 모든 사용자가 사내 직원이면 tenant filter만으로 충분한가?

아닙니다. 같은 tenant 안에서도 팀, 프로젝트, 직무, 계약 기간과 문서 분류가 다릅니다. Tenant는 coarse isolation이고 document authorization은 별도입니다.

Q2. ACL filter를 통과한 document의 모든 chunk는 안전한가?

항상 그렇지 않습니다. Section별 권한 override, 첨부 파일, 표의 별도 classification이 있을 수 있습니다. Resource model과 inheritance rule을 먼저 확인해야 합니다.

Q3. Vector DB가 filter를 지원하면 보안 문제는 끝나는가?

아닙니다. Filter 적용 시점, expression semantics, index projection freshness, cache, citation과 fallback까지 검증해야 합니다. Service 관리자 권한과 최종 사용자 문서 권한도 다릅니다.

Q4. Group 목록을 token에 넣으면 identity service를 매번 호출하지 않아도 되는가?

가능하지만 token TTL 동안 revoke가 늦어질 수 있습니다. Membership revision, 짧은 TTL, introspection 또는 emergency deny를 조합하고 revoke SLO를 측정합니다.

Q5. 권한 서비스 장애 때 public 문서라도 답해야 하는가?

Public corpus가 물리·논리적으로 분리되고 public classification을 독립적으로 증명할 수 있다면 제한 fallback이 가능합니다. 이전 query의 protected cache를 재사용하는 방식은 피합니다.

Q6. 검색 결과 제목만 보여 주는 것은 괜찮은가?

문서 존재와 제목 자체가 민감할 수 있습니다. discover 권한을 따로 두거나 unauthorized resource는 존재하지 않는 것처럼 처리합니다.

마무리

Secure RAG는 prompt에 “기밀을 말하지 마”라고 쓰는 문제가 아닙니다. Unauthorized content가 model에 닿기 전 데이터 경계에서 차단되어야 합니다.

stable tenant and principal identity
  + versioned source-faithful ACL
  + filtered candidate generation
  + downstream authorization propagation
  + fast revoke and cache invalidation
  + adversarial release tests
  = permission-aware Enterprise RAG

다음 글 RAG Freshness·Temporal Retrieval·CDC에서는 content와 ACL 변경을 event time·observed time·indexed time으로 추적하고, out-of-order event와 tombstone을 처리해 “최신”이라는 말을 측정 가능한 SLO로 바꿉니다.

참고문헌

검증 메모 — 공식 문서는 2026년 7월 16일 확인했습니다. Azure AI Search의 일부 native document-level 권한 기능은 해당 시점 preview이므로 실제 API version, 지원 source와 동기화 제약을 다시 확인해야 합니다. 본문의 tenant, group, resource ID와 latency 목표는 가상 예시이며 보안 보증이 아닙니다. 실제 시스템에서는 identity provider·source ACL·vector engine·cache·model provider의 경계를 threat model로 검토하고, 격리된 synthetic corpus로 cross-tenant, revoke, fallback, citation과 trace 누출을 반복 검증해야 합니다.