21장. AI 거버넌스와 보안, 온프레미스 인프라
접근 제어
회사 서류함에는 "공개", "사내", "대외비" 도장이 찍혀 있고, 직급에 따라 열 수 있는 서랍이 다릅니다. 인턴이 "연봉 인상안 요약해 줘"라고 해도 비서가 대외비 서랍에서 서류를 꺼내 와서는 안 됩니다. RAG 에서도 검색하기 전에 그 사람이 볼 수 있는 문서만 남겨야 합니다.
1번에서 로그인한 사용자의 등급과 부서를 확인합니다(13장 인증). 2번에서 문서의 등급과 부서를 보고 볼 수 있는 것만 남깁니다. 3번에서 그 안에서 검색합니다. 4번에서 고른 조각만 LLM 에 넘깁니다. 서랍을 여는 열쇠가 등급, 서랍 앞에서 확인하는 일이 2번입니다.
걸러 내는 때 견주기
| 방법 | 하는 일 | 문제 |
|---|---|---|
| 걸러 내지 않음 | 모든 문서에서 검색 | 비밀 문서가 LLM 에 들어가 답으로 샌다 |
| 검색 뒤 걸러 내기 | 상위 k 개를 찾은 뒤 볼 수 없는 것을 뺀다 | 남는 조각이 줄어 답할 자료가 모자라기 쉽다 |
| 검색 전에 걸러 내기 | 볼 수 있는 문서 안에서만 찾는다 | 문서마다 등급과 부서를 붙여 두어야 한다 |
| "알려 주지 마" 지시 | 프롬프트로 막는다 | 인젝션 한 번에 뚫린다 |
볼 수 있는지 정하는 규칙
def can_read(user, doc):
"""등급이 문서보다 낮지 않아야 한다. 3등급(비밀) 문서는 같은 부서만 본다"""
if user["등급"] < doc["등급"]:
return False
return doc["등급"] < 3 or doc["부서"] in ("공용", user["부서"])
intern = {"이름": "인턴", "등급": 1, "부서": "개발팀"}
hr_lead = {"이름": "인사팀장", "등급": 3, "부서": "인사팀"}
doc = {"제목": "연봉 인상안", "부서": "인사팀", "등급": 3}
dev_secret = {"제목": "보안 사고 대응", "부서": "개발팀", "등급": 3}
# 결과: 인사팀장은 인사 비밀은 보지만 개발팀 비밀은 못 본다
print(can_read(intern, doc), can_read(hr_lead, doc), can_read(hr_lead, dev_secret))코드에 나오는 함수
| 코드 | 쉬운 뜻 |
|---|---|
user["등급"] < doc["등급"] | 사용자의 등급이 문서보다 낮으면 True 다. 그러면 곧바로 False(못 읽음)를 돌려준다 |
doc["등급"] < 3 or ... | 3등급보다 낮은 문서면 여기서 바로 읽을 수 있다. 아니면 or 뒤의 부서 조건을 본다 |
doc["부서"] in ("공용", user["부서"]) | 문서의 부서가 '공용' 이거나 사용자와 같은 부서면 True 다 |
이 코드가 뜻하는 것
호텔 카드키를 떠올려 봅니다. 카드키의 등급이 층보다 낮으면 엘리베이터가 그 층에 서지 않습니다. 가장 높은 층의 객실은 등급이 맞아도 자기 방이나 공용 라운지만 열립니다. 여기서 카드키 등급이 사용자 등급, 가장 높은 층이 3등급 문서, 자기 방이 같은 부서입니다.
| 줄 | 하는 일 | 쉽게 말하면 |
|---|---|---|
if user["등급"] < doc["등급"]: return False | 등급이 모자라면 바로 거절 | 엘리베이터가 서지 않는다 |
return doc["등급"] < 3 or ... | 낮은 등급 문서는 허락, 3등급은 부서까지 본다 | 높은 층은 자기 방만 열린다 |
intern, hr_lead, doc, dev_secret | 사용자 둘과 문서 둘(지어낸 예) | 손님 둘, 객실 둘 |
결과 False True False 는 차례로 인턴은 연봉 인상안을 못 보고, 인사팀장은 볼 수 있고, 인사팀장도 개발팀의 비밀 문서는 못 본다는 뜻입니다.
작게 줄여서 눈으로 보기
인사팀 3등급 사용자 한 명을 두고, 문서 네 가지를 차례로 넣어 봅니다. 문서는 설명하려고 지어낸 것입니다.
def can_read(user, doc):
if user["등급"] < doc["등급"]:
return False
return doc["등급"] < 3 or doc["부서"] in ("공용", user["부서"])
user = {"등급": 3, "부서": "인사팀"}
for doc in [{"등급": 1, "부서": "개발팀"}, {"등급": 3, "부서": "공용"},
{"등급": 3, "부서": "인사팀"}, {"등급": 3, "부서": "개발팀"}]:
print(doc, "→", can_read(user, doc)){'등급': 1, '부서': '개발팀'} → True
{'등급': 3, '부서': '공용'} → True
{'등급': 3, '부서': '인사팀'} → True
{'등급': 3, '부서': '개발팀'} → False| 결과 | 읽는 법 |
|---|---|
{'등급': 1, '부서': '개발팀'} → True | 3등급보다 낮은 문서는 부서가 달라도 읽는다 |
{'등급': 3, '부서': '공용'} → True | 3등급이지만 공용 문서라 읽는다 |
{'등급': 3, '부서': '인사팀'} → True | 같은 부서의 비밀 문서라 읽는다 |
{'등급': 3, '부서': '개발팀'} → False | 등급은 충분해도 다른 부서의 비밀 문서라 못 읽는다 |
등급과 부서, 두 가지를 함께 봐야 "높은 사람은 다 본다"는 빈틈이 막힙니다. 이 규칙은 검색 전에 문서를 걸러 낼 때 씁니다.