사내 문서 RAG 접근 제어: 검색 단계에서 권한을 거르는 구현 방법

pgvector 권한 필터 쿼리, HNSW 필터링 함정, 권한 변경 반영, 검색 로그까지 OWASP LLM08 기준으로 정리

사내 RAG 접근 제어 썸네일

핵심 요약

  1. 권한 확인은 답변 생성 후가 아니라 검색 쿼리 단계에서 해야 한다. 모델에 전달된 내용은 되돌릴 수 없다.
  2. OWASP LLM08은 권한을 인식하는 벡터 저장소, 수집 데이터 검증, 변경 불가능한 검색 로그를 권한다.
  3. pgvector의 HNSW 인덱스는 WHERE 필터를 스캔 후 적용하므로 결과가 모자랄 수 있고, hnsw.iterative_scan으로 보완한다.
  4. 원본 권한 변경과 삭제를 인덱스와 답변 캐시에 반영하는 절차가 필요하다.

사내 문서 RAG에서 가장 곤란한 사고는 답변이 틀리는 것이 아니라, 질문한 사람이 볼 권한이 없는 문서 내용이 답변에 섞여 나오는 것이다. 인사 평가 문서의 한 문단이 일반 직원의 질문에 근거로 인용되는 식이다. 많은 구현이 답변을 만든 뒤 권한을 확인하거나 화면에서 출처 링크만 가리는데, 그 시점이면 내용은 이미 모델에게 전달된 뒤다. 이 글은 권한 필터를 검색 단계에 넣는 방법을 PostgreSQL과 pgvector 예시로 정리한다.

OWASP가 꼽은 벡터·임베딩 위험

OWASP LLM08:2025는 RAG의 벡터·임베딩 계층에서 생기는 위험을 따로 다룬다. 사내 문서 RAG에 직접 해당하는 항목과 대응을 정리하면 다음과 같다.

위험사내 RAG에서의 모습대응
무단 접근·데이터 유출접근 제어가 없거나 원본 문서 권한과 어긋난 벡터 저장소에서 민감 문서가 검색됨권한을 인식하는 세분화된 접근 제어를 검색 쿼리에 적용
맥락 간 정보 유출부서·고객사를 한 인덱스에 섞어 다른 그룹 문서가 섞여 나옴그룹 단위 필터 또는 인덱스 분리
데이터 오염검증되지 않은 문서가 수집돼 잘못된 근거가 됨수집 파이프라인에서 출처·형식 검증
임베딩 역변환임베딩에서 원문 내용이 상당 부분 복원됨임베딩도 원문과 같은 등급으로 보호

OWASP는 검색 활동을 변경할 수 없는 로그로 자세히 남겨 이상 행동을 빨리 찾으라고도 권한다. 임베딩 역변환 항목이 말해 주듯, "벡터는 숫자라서 괜찮다"는 가정은 성립하지 않는다.

검색 쿼리에 권한 조건 넣기

가장 단순하고 확실한 방법은 청크마다 접근 가능한 그룹을 저장하고, 검색 쿼리의 조건에 요청자의 그룹을 넣는 것이다. 그룹 목록은 클라이언트가 보낸 값이 아니라 서버가 인증 정보로 조회한 값이어야 한다.

CREATE EXTENSION IF NOT EXISTS vector;

CREATE TABLE doc_chunks (
  id             bigserial PRIMARY KEY,
  doc_id         text   NOT NULL,
  allowed_groups text[] NOT NULL,   -- 원본 문서의 권한을 수집 시점에 복사
  content        text   NOT NULL,
  embedding      vector(1536)       -- 사용하는 임베딩 모델의 차원에 맞춘다
);

CREATE INDEX ON doc_chunks USING hnsw (embedding vector_cosine_ops);

-- $1: 서버가 조회한 요청자 그룹 목록, $2: 질문 임베딩
SELECT doc_id, content
FROM doc_chunks
WHERE allowed_groups && $1::text[]
ORDER BY embedding <=> $2
LIMIT 8;

&&는 두 배열에 겹치는 값이 있는지 보는 PostgreSQL 연산자이고, <=>는 pgvector의 코사인 거리 연산자다. 애플리케이션 코드가 조건을 빠뜨리는 실수까지 막고 싶다면 같은 조건을 행 수준 보안(RLS) 정책으로 데이터베이스에 걸어 둘 수 있다. Supabase를 쓴다면 RLS 문서의 정책 형식을 따르면 된다. RLS 설정 방법은 바이브 코딩 사내 도구 보안 설정에서도 다뤘다.

HNSW 인덱스와 필터를 같이 쓸 때의 함정

권한 필터를 넣으면 이번에는 결과가 모자라는 문제가 생긴다. pgvector README에 따르면 HNSW 같은 근사 인덱스를 쓸 때 WHERE 조건은 인덱스를 훑은 뒤에 적용된다. 기본 후보 수(hnsw.ef_search, 기본값 40) 안에 권한 있는 청크가 적으면, LIMIT 8을 요청해도 서너 건만 돌아올 수 있다. 권한 범위가 좁은 사용자일수록 답변 품질이 떨어지는 이상한 현상이 생기는 이유다.

-- 필터 조건을 만족하는 결과가 충분할 때까지 인덱스를 더 탐색
SET hnsw.iterative_scan = strict_order;

README는 반복 인덱스 스캔(iterative index scans)을 켜면 필요한 만큼 인덱스를 더 탐색하면서 거리 순서를 유지한다고 설명한다. 그룹 수가 적고 그룹별 문서가 많다면 그룹마다 인덱스나 테이블을 나누는 방법도 검토할 수 있다. 벡터 데이터베이스 선택 자체가 고민이라면 pgvector, Pinecone, Milvus 비교를 참고한다.

권한 변경과 삭제를 인덱스에 반영하기

수집 시점에 복사한 권한은 원본이 바뀌면 낡은 정보가 된다. 퇴사자의 문서 접근이 회수됐는데 벡터 저장소에는 옛 권한이 남아 있는 식이다. 다음 절차를 수집 파이프라인에 포함한다.

  1. 원본 저장소의 권한 변경·삭제 이벤트(또는 정기 비교)로 바뀐 doc_id 목록을 만든다.
  2. 해당 doc_id의 모든 청크에서 allowed_groups를 갱신하거나 청크를 삭제한다.
  3. 답변 캐시가 있다면 같은 문서를 근거로 만든 캐시 항목도 무효화한다.
  4. 반영 지연 시간(원본 변경 → 검색 반영)을 측정해 운영 기준으로 관리한다.

폐쇄형의 범위를 정확히 정의하기

"폐쇄형 RAG"라고 해도 임베딩 생성, 답변 생성에 외부 API를 쓰면 문서 내용은 그 서비스로 전송된다. 외부 모델을 쓸 경우 해당 서비스의 데이터 보관·학습 정책을 계약 요금제 기준으로 확인하고, 등급이 가장 높은 문서는 수집 대상에서 제외하거나 내부 모델로만 처리하는 식으로 등급별 경로를 나눈다. 답변에 원문 링크를 붙여 사용자가 근거를 확인하게 하는 방법은 RAG 검증과 출처 관리에서 다룬다.

초보자가 자주 실수하는 포인트

  • 답변을 생성한 뒤 화면에서 출처만 가려 권한 문제를 해결했다고 보는 경우
  • 요청자 그룹 목록을 클라이언트가 보낸 값 그대로 검색 조건에 쓰는 경우
  • 권한 필터를 추가한 뒤 결과 건수가 줄어드는 현상을 모델 성능 문제로 오해하는 경우
  • 임베딩은 원문이 아니라며 원문보다 낮은 보안 등급으로 저장하는 경우

체크리스트

  • 청크마다 원본 문서의 권한 정보를 저장했는가
  • 검색 쿼리의 권한 조건을 서버가 조회한 그룹으로 구성하는가
  • 권한이 좁은 계정으로 검색해 결과 건수가 충분한지 확인했는가
  • 권한 변경·삭제가 인덱스와 캐시에 반영되는 절차와 지연 시간을 관리하는가
  • 검색 요청과 반환된 문서 ID를 로그로 남기는가

자주 묻는 질문

부서별로 인덱스를 완전히 나누는 편이 더 안전하지 않나요?

그룹 경계가 명확하고 수가 적다면 인덱스 분리가 가장 단순하고 확실하다. 다만 한 문서를 여러 그룹이 공유하거나 권한이 자주 바뀌면 중복 저장과 동기화 부담이 커지므로, 그 경우에는 공유 인덱스에 권한 필터를 거는 방식이 관리하기 쉽다.

참고 자료 · 검증 기준

위 자료와 내용을 대조한 날짜: . 도구·서비스 정책은 이후 바뀔 수 있으므로 적용 전 공식 문서를 다시 확인하세요.

이 글은 실제 실무 환경 테스트 및 검증된 기술 자료를 바탕으로 작성되었으며, 환경 변화에 따라 지속적으로 갱신됩니다.