사내보안 규정 · 감사 로그 분석 AI 에이전트
같은 질문을 던져도 부서와 등급에 따라 근거 조항이 달라집니다. 권한 검사가 검색보다 먼저 일어나기 때문에, 가려진 문서는 결과 개수로도 순위의 빈자리로도 존재를 드러내지 않습니다.
데모 백엔드는 상시 가동이 아닙니다 — 24시간 켜두지 않는 무료 구성이라, 꺼져 있으면 화면이 그 사실을 명시합니다.
01 · 프로젝트 개요
사내 규정과 감사 로그는 누구나 다 볼 수 있는 자료가 아닙니다. 인사 징계 기준, 임원 성과급 산정, 특정 서버의 접속 기록 — 읽을 수 있는 사람이 정해져 있습니다.
일반적인 RAG 는 유사도 상위 k 건을 먼저 뽑고 그 다음 권한으로 거릅니다. 그러면 결과 개수 자체가 신호가 됩니다 — “10건 요청했는데 3건만 왔다”는 사실이 권한 밖에 7건이 있다는 것을 알려줍니다.
권한 조건을 ORDER BY·LIMIT 보다 앞선
WHERE 절에 둡니다. 볼 수 있는 문서 안에서만 상위 k 를 고르므로,
허용된 조항이 k 건 이상이면 누가 물어도 개수가 같습니다.
“권한 검사를 했다”가 아니라 “사후 필터링으로 되돌리면 실패한다”를 테스트로 못 박았습니다. 의도적으로 새는 경로를 코드베이스 안에 두고, 그것이 실제로 새는 것을 테스트가 확인합니다.
차별성
등급이 높을수록 많이 보는 것이 아닙니다. 부서와 등급 두 축이 동시에 걸리기 때문에, 최고 등급이면서도 못 보는 문서가 존재합니다. 이 프로젝트는 그것을 말로 설명하지 않고 실측값으로 보여줍니다.
| 질의 | 김개발개발팀 · 1 | 한보안정보보안팀 · 2 | 최임원경영지원팀 · 3 | 무엇이 갈랐나 |
|---|---|---|---|---|
| 비밀번호는 얼마나 자주 바꿔야 하나 | 동일 | 동일 | 동일 | 아무것도열 계정 전부 같은 조항 집합 |
| 운영 서버 접속에 어떤 승인이 필요한가 | 5.1.1 수신 | 2.6.1 | 2.6.1 | 부서개발팀만 개발팀 전용 규정에 닿는다 |
| 임원 성과급은 어떤 기준으로 정해지나 | 2.2.x | 2.2.x | 6.1.x 수신 | 등급등급 3 두 명만 임원 문서에 닿는다 |
| 침해사고 접수와 보고 기한 | 미수신 | 5.3.1 · 5.3.3 | 미수신 | 둘 다최고 등급도 부서가 다르면 못 본다 |
2026-09-03 실측 · 하이브리드 검색 k=10 · 비교 대상은 clause_code 집합.
마지막 행이 이 프로젝트의 논지입니다 — 최임원은 등급 3이지만 정보보안팀 문서를 받지 못합니다.
02 · 시스템 구성
core/ 는 바깥 계층이나 프레임워크를 import 하지 않습니다.
그 규칙은 문서가 아니라 CI 에서 강제됩니다 — 어기면 빌드가 실패합니다.
권한 판정 visible(), 하이브리드 검색 융합(RRF), 에이전트 정책.
DB·HTTP·LLM 을 모릅니다. 포트(ports.py)로만 바깥과 말합니다.
pgvector 검색, 로그 검색, 임베딩 모델, Gemini 호출. 포트를 구현할 뿐 도메인 규칙을 갖지 않습니다.
FastAPI 라우터. 의존성 주입으로 core 와 adapters 를 엮습니다.
요청 스키마에 department·clearance 가 없습니다.
브라우저가 닿는 것은 Vercel 의 라우트 핸들러 하나뿐입니다.
백엔드 주소도 공유 시크릿도 브라우저로 내려가지 않고,
권한 값은 요청 본문에 실리지 않습니다 — 서버가 페르소나 이름을
principals 테이블에서 등급으로 번역합니다. 클라이언트가 자기 등급을
보낼 수 있으면 그것이 곧 사칭 경로이기 때문입니다.
화면
방문자는 허브에서 직원 한 명을 고르고 그 사람이 되어 서비스를 씁니다. 직원 면에는 페르소나 선택기가 없습니다 — 내가 그 사람이기 때문입니다. 비교하는 도구는 설명 면과 관리자 면에만 둡니다.
실행 중인 애플리케이션을 그대로 캡처했습니다. 한 곳만 손댔습니다 —
로그인 계정 자리의 메일 주소를 demo@example.com 으로 바꾸고 찍었습니다.
그 밖의 숫자와 문장은 전부 실제 화면의 것입니다.
열 명의 시연 계정이 부서 4개 × 등급으로 격자를 이룹니다. 지금 보는 중인 계정은 카드에 표시됩니다.
김개발(개발팀·등급 1)이 운영 서버 접속을 묻자 개발팀 전용 규정 5.1.1 이 근거로 붙었습니다. 요구 등급은 1 이라 누구나 넘지만 부서가 다르면 등급 3 계정에도 오지 않습니다.
목록에 없는 문서는 제목조차 브라우저로 오지 않습니다. 화면은 이 목록을 고른 코드가 실제 강제 지점(SQL)의 사본이라는 사실까지 적어둡니다.
계정을 바꿔가며 같은 코퍼스가 어떻게 갈리는지 봅니다. 허용 부서가 비어 있으면 전사 공개라는 규칙까지 화면에 적혀 있습니다.
사전 필터링과 순진한 사후 필터링을 그 자리에서 여섯 번 검색해 비교합니다. LLM 을 부르지 않아 질의 횟수를 쓰지 않습니다.
권한은 호스트에 걸립니다. 같은 호스트 안에 다른 사람의 기록이 섞여 있을 수 있다는 사실을 화면이 먼저 말합니다.
시연용으로 채운 데이터가 아니라 이 배포에서 실제로 일어난 요청만 보여줍니다. 표본이 적으면 적다고 적습니다.
기술 스택
고른 이유와 그 대가를 함께 적습니다. 포트폴리오 운영 비용을 0원으로 유지하는 것이 이번 배포의 제약이었습니다.
03 · 개발 원칙
저는 빠르게 만들고 여러 번 검증해서 마무리합니다. 속도를 택하면 보통 안전성을 잃는다고들 합니다. 그래서 보안을 주제로 잡았습니다 — 취약점이 치명적인 도메인에서 속도를 유지하면서 신뢰할 수 있는 결과를 내는 방법을 찾아보려고요.
AI 도구는 코드를 사람이 읽는 속도보다 빠르게 만듭니다.
그래서 모든 라인을 눈으로 좇는 대신, 틀리면 반드시 터지는 장치를 먼저 세웁니다.
제품 화면의 모든 주장은 코드·스키마·실측 중 하나를 출처로 갖습니다. “세 계정이 같은 조항을 받는다”라고 적으려면 먼저 재봐야 하고, 계정이 열 명이 되면 그 문장은 다시 재서 고쳐야 합니다.
실제로 이 규칙이 화면의 거짓 문장 넷을 잡았습니다 — 전부 “파일을 인용했지만 그 파일이 그렇게 말하는지 확인하지 않은” 형태였습니다.
누출을 막았다고 적는 대신, 의도적으로 새는 경로를 코드베이스에 남겨두고 그것이 실제로 샌다는 것을 테스트로 확인합니다. 방어가 무너지면 그 테스트가 먼저 터집니다.
새는 경로는 demo/ 한 곳에 격리하고, 그 디렉토리를 import 할 수 있는
파일 목록을 집합 동일성으로 고정합니다 — 셋째 파일이 생기면 테스트가 먼저 터집니다.
로딩 중이거나 백엔드가 꺼져 있을 때 “0건”이라고 쓰면, 사용자는 확인된 사실로 읽습니다. 보안 알림 패널에서는 특히 그렇습니다. 실패·미확인·0 은 화면에서 서로 다른 상태여야 합니다.
권한 규칙은 SQL(사전 필터링)·Python(재검증)·TypeScript(화면 표시) 세 계층에 의도적으로 삽니다. 각자 다른 일을 하기 때문입니다. 문제가 되는 것은 같은 계층 안에서 사본이 늘어나는 쪽이고, 그건 테스트가 셉니다.
04 · 개발 일정 및 진행 현황
2026년 8월 31일 착수 · 3~4주 계획. 4일차인 오늘 기준 코어 기능과 배포가 끝났고, 남은 기간은 확장과 다듬기에 씁니다.
ISMS-P 인증기준 안내서 255쪽을 조항 단위로 분할해 pgvector 에 적재.
벡터·키워드 하이브리드 검색을 RRF 로 융합. 계층 경계 테스트를 CI 에 걸어
core/ 의 의존 방향을 처음부터 강제.
권한이 실제로 갈리는 합성 규정 코퍼스를 세우고, 사후 필터링이면 반드시 실패하는 누출 테스트를 작성. 검색 품질을 숫자로 재는 평가 하네스 구축.
LangGraph 도구 호출 루프, FastAPI 엔드포인트, Google 로그인 뒤의 Next.js 화면 8개와 관리자 대시보드. 목 데이터를 화면에 두지 않는다는 규칙을 이때 세움 — 지어낸 값 대신 진짜 데이터를 만들거나 없다는 사실을 명시.
운영 로그에 호스트 기준 권한을 걸고 query_logs 도구를 추가.
부분 집계를 전체로 오해시키지 않도록 무조건적 범위 고지를 붙임.
순진한 RAG 가 새는 것을 화면에서 실행해 보이는 /how 구현.
Vercel + Cloudflare Tunnel + 로컬 Docker 구성으로 무료 티어 안에서 공개. 도메인을 이전 프로젝트에서 이 프로젝트로 이전. 공개 저장소에 시크릿이 들어 있던 것을 이때 발견해 회전하고, 설정을 fail-closed 로 바꿈.
화면을 직원 · 설명 · 관리자 세 면으로 재구성. 방문자가 직원 한 명을 고르면 그 사람이 되어 쓰고, 로그아웃하면 다시 고릅니다. 직원 면에서 페르소나 선택기를 걷어냄 — 내가 그 사람이므로 고를 것이 없습니다.
900px 한 갈래로 셸을 접고 좁은 화면의 넘침 세 곳을 막음. 시연 계정을 3명에서 10명으로 늘려 부서 4개 × 등급 격자를 채우고, 그 과정에서 드러난 화면 결함 일곱 개를 수정. 부서 이름이 어긋나 아무에게도 보이지 않던 문서 하나를 이때 발견.
평가 하네스 지표 확대, 코퍼스 규모를 키운 누출 재현 실험, 문서 업로드 파이프라인, 접근 이상 탐지 규칙. 아래 확장·BM 절에 정리했습니다.
05 · 개발 리뷰
“검증 체계를 만들었다”가 아니라 “그 체계가 이것들을 잡았다”를 기록합니다. 아래는 이 프로젝트에서 실제로 걸린 결함들입니다.
백엔드를 공개하면서 docker-compose.yml 에 개발용 시크릿이 그대로 있었습니다.
회전하고, 설정 파일이 값 없이는 기동에 실패하도록 바꿨습니다.
다른 프로젝트의 네트워크에 backend 라는 같은 이름으로 붙어
기존 서비스 요청의 절반이 잘못된 컨테이너로 갔습니다.
즉시 되돌리고 서비스 이름을 분리한 뒤, 그 이유를 설정 파일에 적어뒀습니다.
새는 데모 경로를 demo/ 에 가뒀는데, from api.demo import compare 로
한 단계 건너 닿을 수 있었습니다. 탐지기가 최상위 이름만 보고 있었던 탓입니다.
두 경로를 모두 오염으로 세도록 고치고, 두 번째 진입점을 목록에 정직하게 올렸습니다.
정렬 안정성 테스트가 수정 전후로 똑같이 통과했습니다 — 현재 코퍼스 규모에서는 그 현상이 애초에 나타나지 않았기 때문입니다. SQL 문자열의 절 위치를 직접 확인하는 테스트로 교체했습니다.
규정 문서 하나의 허용 부서가 보안팀 인데 계정·호스트 어디에도 그 문자열이
없었습니다. 부서 이름이 조인 키라 그 문서는 존재하지만 누구도 검색할 수 없었습니다.
계정을 10명으로 늘리며 격자를 채우다 드러났습니다.
문서 가시성 화면이 목록의 첫 사람 기준으로 그리면서 “현재 페르소나 기준”이라고 적고 있었습니다. 한보안으로 들어온 사람에게 김개발의 가시성을 보여주며 그것이 당신 기준이라고 말한 셈입니다.
06 · 확장성과 사업화
사내 규정 챗봇은 이미 많습니다. 도입을 막는 것은 성능이 아니라 “그 봇이 못 볼 문서를 정말 못 보는지 어떻게 증명하느냐”입니다. 이 프로젝트는 그 증명을 산출물로 갖고 있습니다.
권한 판정이 SQL 한 문장에 모여 있고, 그 문장이 어디에 쓰이는지를 테스트가 집합으로 고정합니다. 보안 심사에서 “어디를 보면 되느냐”에 파일 하나와 테스트 하나로 답할 수 있습니다.
대부분의 구현은 내용 노출만 막습니다. 이 프로젝트는 결과 개수·순위· 부분 집계까지 관측 채널로 보고 각각에 장치를 답니다. 규제 산업일수록 이 차이가 커집니다.
규정 문서와 서버 로그가 같은 권한 판정 함수를 공유합니다. 보안팀이 실제로 다루는 두 자산을 한 제품에서 같은 정책으로 묶습니다.
principals 테이블이 진실입니다.
사내 IdP(Okta·Azure AD)의 그룹을 그대로 부서·등급으로 번역하는 어댑터가
다음 단계입니다. 포트가 이미 분리돼 있어 core 는 바뀌지 않습니다.access_records 에 누가 무엇을
언제 열람했는지 쌓고 있습니다. 여기에 규칙을 걸면 별도 수집 없이
이상 열람 알림이 됩니다.가설입니다. 고객 인터뷰로 검증한 내용이 아니라는 점을 명시합니다.
07 · 부록
커밋마다 무엇을 왜 했는지 남겼습니다. 결정의 이유와 그 대가가 함께 적혀 있습니다.
2026-08-31 부터 오늘까지, 영역별로 나눈 전체 기록.
전체 보기 →
검증갖춘 장치, 그 장치가 잡아낸 것, 그리고 이 방식의 한계.
전문 보기 →
소스백엔드·프론트엔드·인프라 설정·구현 계획서 전부 공개.
저장소 열기 ↗
Jekyll + GitHub Pages 로 빌드합니다. 개발 로그는 jekyll/_logs/ 의
컬렉션이고, 커밋할 때마다 파일이 하나씩 늘어납니다 —
이 페이지의 숫자도 그 파일들을 세서 나옵니다.
화면 스크린샷은 실행 중인 애플리케이션에서 직접 캡처한 것이며,
표에 적힌 실측값은 라이브 백엔드에 질의해 얻은 값입니다.
Contact
열 명의 직원 중 아무나 골라 같은 질문을 던져보세요. 누구로 묻느냐에 따라 근거 조항이 어떻게 갈리는지 바로 보입니다.