theAuth: AI 에이전트 액션에 대한 컴플라이언스 로깅 및 감사 추적
요약
본 가이드는 AI 에이전트가 수행하는 모든 의사결정 과정을 기록하고 감사 추적을 가능하게 하는 오픈 소스 인증 시스템 'theAuth'를 소개합니다. 이를 통해 누가, 언제, 어떤 고객 기록에 접근했는지 등 컴플라이언스 요구사항을 충족할 수 있습니다. 개발 과정에서 에이전트의 행동 로그와 의심스러운 활동 경고 기능 등을 구축하는 방법을 다룹니다.
핵심 포인트
- AI 에이전트의 모든 의사결정 순간을 기록하여 감사 추적(Audit Trail)을 구현합니다.
- theAuth는 오픈 소스 TypeScript 라이브러리로, 컴플라이언스 로깅 및 권한 관리를 지원합니다.
- 로그 외에도 경고 기능, 데이터 보존 작업, GDPR 경로 등을 제공하여 안정적인 시스템 구축이 가능합니다.
theAuth는 AI 에이전트와 인간을 위한 오픈 소스 인증(auth) 시스템입니다. GitHub에서 리포지토리 스타하기 · 문서 읽기 · 빠른 시작하기 · theauth.dev
출시 후 6개월 뒤에 받은 이메일 속 질문을 상상해 보세요. "어떤 에이전트가 3월 3일에 고객 기록 4417에 접근했나요? 그리고 누가 승인했나요?" 만약 당신의 대답이 "앱 로그에서 grep 해보겠습니다."로 시작한다면, 앞으로 이틀간 어떤 일이 벌어질지 이미 알고 계실 겁니다.
저 역시 그런 질문을 받은 경험이 있습니다. 에이전트는 일반 서비스보다 상황이 더 심각합니다. 왜냐하면 에이전트는 런타임(runtime)에 무엇을 호출할지 결정하기 때문입니다. 코드를 읽는다고 해서 어떤 작업을 했는지 알 수 없습니다. 모든 의사 결정 순간마다 기록된 로그가 필요합니다.
본 가이드에서는 이 기록을 처음부터 구축합니다. 우리는 에이전트의 신원 및 권한 부여를 위한 오픈 소스 TypeScript 라이브러리인 theAuth를 사용할 것입니다. 끝날 무렵에는 모든 에이전트 의사 결정을 포착하는 로그, 의심스러운 행동에 경고(alerts)가 울리는 기능, 보존 작업(retention job), 사용자 삭제를 위한 GDPR 경로, 그리고 검토자에게 제출할 수 있는 내보내기(export) 기능을 갖게 될 것입니다.
또한 라이브러리가 무엇을 하지 않는지도 알려드리겠습니다. 문서를 자세히 읽다가 놀란 부분이 몇 가지 있었는데, 감사관이 알기 전에 여러분이 들어야 합니다.
이 내용은 theAuth 가이드의 8번째 중 8번째입니다. 독립적으로 작동하므로 여기서 바로 시작할 수 있습니다. 또한 가이드 6과 7과도 잘 어울립니다.
TL;DR
| 목표 | 사용하는 기능 | 위치 |
|---|---|---|
| 모든 에이전트 결정 기록 | agents.auditAll (기본 활성화) | 감사 추적 |
| ... |
Node 프로젝트가 이미 있는 경우 약 45분이 소요될 것으로 예상됩니다. 코드는 로컬에서는 SQLite를, 프로덕션 환경에서는 Postgres에서 실행됩니다.
사전 준비 사항 (Prerequisites)
Node 20 이상, TypeScript 프로젝트, 그리고 다음 패키지가 필요합니다:
npm install @glinr/theauth
또한 users 테이블의 행으로 존재하는 사용자 ID가 하나 필요합니다. 모든 에이전트는 소유자(owner)를 가지며, 이 소유자 열은 외래 키입니다. 빠른 시작 (quickstart)에서는 인증 제공업체가 다른 곳에 있는 경우 해당 행을 생성하는 방법을 보여줍니다.
시작하기 전에 솔직하게 말씀드릴 점이 있습니다. theAuth는 컴플라이언스 작업을 위한 빌딩 블록을 제공합니다. 하지만 사용자가 자동으로 컴플라이언스를 충족시켜 주는 것은 아닙니다. 문서에는 세 개의 별도 페이지에 걸쳐 그렇게 명시되어 있으며, 저 역시 이에 동의합니다. 통제 매핑(control mapping)은 보고를 돕는 보조 수단입니다. 귀하의 프로세스, 법률 검토, 그리고 자문 변호사가 의무 준수 여부를 결정합니다.
단계 1: 인스턴스를 생성하고 auditAll을 활성화 상태로 유지하기
감사 로그(audit log)는 권한 엔진의 부가 효과입니다. 사용자가 수동으로 로그 라인을 작성하지 않습니다. 인스턴스를 생성하면, authorize()를 호출할 때마다 엔진이 한 행을 기록합니다.
import { createTheAuth } from '@glinr/theauth';
export const theauth = await createTheAuth({
...
⚠️ [IMG:N] 형식 토큰은 이미지 placeholder 입니다. 번역하지 말고 원래 위치에 그대로 유지하세요.
The auditAll 플래그는 기본값으로 true입니다. 이를 false로 설정하면 거부(denial) 건이라도 권한 엔진이 아무 행(row)도 기록하지 않습니다. 이는 위험할 수 있는 부분이므로, 검토자가 확인할 수 있도록 구성 파일에 이 플래그를 명시적으로 유지합니다. 구성 참조에는 모든 에이전트 옵션이 나열되어 있습니다.
배포 시 데이터베이스 블록을 { provider: 'postgres', url: process.env.DATABASE_URL! }로 교체하세요. SQLite는 노트북에서는 괜찮지만, 여러 프로세스가 로그에 쓰기 시작하면 Postgres가 올바른 선택입니다.
2단계: 모든 에이전트에 소유자(owner)와 제한된 권한 부여하기
감사 기록은 누군가를 명시해야만 유용합니다. 각 에이전트는 ownerId를 가지고 있으며, 이는 해당 에이전트가 작성하는 모든 행의 userId가 됩니다. 이 연결 고리가 전체 출처(provenance) 체인입니다: 이 행동을 누가 소유한 에이전트가 했는지.
const agent = await theauth.agent.create({
ownerId: 'user-123',
name: 'support-summarizer',
...
권한의 형태에 주목하세요. 에이전트는 티켓을 광범위하게 읽을 수 있지만, 환불(refund) 행동은 제한적이고, 비율 제한(rate limited)되며, 승인 절차 뒤에 보호됩니다. 제한된 권한은 로그를 읽기 쉽게 만듭니다. 에이전트가 모든 것을 할 수 있다면, 모든 행이 똑같이 보일 것이고 로그는 아무것도 알려주지 않습니다.
에이전트 토큰은 생성 시 한 번 나타납니다. theAuth는 SHA-256 해시만 저장합니다. 토큰을 잃어버리면 순환(rotate)해야 합니다.
승인 흐름 자체는 승인 흐름을 참조하세요. 빠른 시작 가이드에서 주의할 점이 있습니다: requireApproval: true를 사용하면, 승인 처리를 연결하기 전까지 authorize()가 거부(denies)합니다. 감사 로그는 이를 다른 어떤 거부와 마찬가지로 기록합니다.
3단계: 모든 민감한 행동 전에 authorize 호출하기
이제 로그를 채우는 부분입니다. 에이전트가 중요한 것에 손대기 전에 authorize()를 호출하세요.
const decision = await theauth.authorize(agent.id, {
action: 'read',
resource: 'mcp:tickets:4417',
...
반환되는 auditId는 엔진이 기록한 행의 기본 키와 같습니다. 이 ID를 자체 애플리케이션 로그, 트레이스 스팬(trace spans), 그리고 사용자에게 노출하는 모든 오류에 포함시키십시오. 나중에 단일 ID가 여러분의 앱 로그를 감사 추적(audit trail)과 연결해 줍니다.
만약 서비스가 수신된 요청으로부터 원시 베어러 토큰(raw bearer token)을 받는 경우, 대신 authorizeByToken(token, { action, resource })을 사용하십시오. 이 함수 역시 동일한 종류의 항목을 기록합니다.
행이 포함하는 내용
각 항목에는 에이전트 ID, 사용자 ID, 수행된 액션, 리소스, 전달한 매개변수, 결과, 거부 시의 자유 텍스트 사유, 밀리초 단위의 평가 시간, 그리고 타임스탬프가 기록됩니다. 이 테이블은 또한 요청에 IP 주소와 사용자 에이전트(user agent)가 포함된 경우 해당 정보도 보관합니다.
컴플라이언스 작업에는 두 가지 세부 사항이 중요합니다.
첫째, parameters 필드는 authorize()에 전달하는 인수를 저장합니다. 만약 여기에 고객의 이메일 주소나 원시 프롬프트(raw prompt)를 전달한다면, 이는 감사 테이블에 기록됩니다. 이는 조사에는 유용하지만 개인 정보 보호 측면에서는 책임 소재가 될 수 있습니다. 저는 식별자(identifiers)와 필드 이름만 전달하며, 사용자로부터 받은 자유 텍스트는 절대 전달하지 않습니다.
둘째, 로그가 모든 것을 기록하는 것은 아닙니다. 문서에 명시되어 있듯이, 권한 엔진이 실행되기 전에 거부된 호출(예: 알 수 없거나 취소된 에이전트)은 아무런 행도 기록하지 않습니다. API를 반복적으로 호출하며 실패하는 취소된 에이전트는 감사 테이블에 흔적을 남기지 않습니다. 만약 이 정보가 필요하다면 게이트웨이(gateway) 단계에서 이를 포착해야 합니다.
4단계: 로그 조회
일일 검토에 필요한 모든 것은 하나의 메서드에서 나옵니다. 필터는 선택 사항이며 조합 가능하고, 결과는 최신순으로 반환됩니다.
const denials = await theauth.audit.query({
agentId: agent.id,
result: 'denied',
...
limit을 지정하지 않으면 쿼리는 일치하는 모든 항목을 반환합니다. 메모리 측면에서 좋지 않은 날이 될 수 있는 바쁜 테이블이므로, 항상 애플리케이션 코드에 limit을 설정하십시오.
스티커 메모를 붙일 만한 함정(gotcha) 하나가 있습니다: actions 필터는 limit과 offset 실행 후에 작동합니다. 페이지 결과가 요청한 limit보다 짧게 나올 수 있습니다. 액션 이름으로 필터링하는 경우, 빈 페이지가 나올 때까지 결과를 탐색하고, 짧은 페이지를 끝이라고 간주하지 마십시오.
동일한 데이터는 HTTP를 통해 이용 가능합니다. REST API 레퍼런스에는 GET /audit와 내보내기(export) 엔드포인트가 문서화되어 있습니다. 경고문을 두 번 읽으십시오. 어댑터들은 감사 쿼리(audit queries)를 포함한 대부분의 엔드포인트에서 호출자를 인증하지 않으므로, 마운트 경로 앞에 자체 인증을 구현해야 합니다. 열린 감사 엔드포인트는 그 자체가 하나의 사고입니다.
만약 쿼리하는 것보다 클릭하는 것을 선호한다면, 관리자 대시보드에는 필터링 가능한 로그 뷰어와 내보내기 기능이 있습니다.
5단계: 로그를 기반으로 이상 탐지 구축하기
여기서 직설적으로 말해야 할 부분이 있습니다. theAuth는 이상 탐지기(anomaly detector)를 제공하지 않으며, 라이브러리에는 scan() 함수가 없습니다. 이상 페이지에는 존재하는 것만 나열되어 있습니다. 즉, anomalyCount 요소를 가진 신뢰 점수(trust score), onViolation 훅, 그리고 아무것도 방출하지 않는 예약된 anomaly.detected 이벤트 이름입니다.
이는 '이상(anomaly)'이라는 단어가 암시하는 것보다 적은 기능이며, 지금 아는 것이 낫습니다. 좋은 소식은 감사 로그에 간단한 탐지기가 필요로 하는 모든 것이 있다는 것입니다. 두 가지 쿼리만으로 제가 신경 쓰는 대부분의 것을 커버할 수 있습니다.
에이전트별 거부율(Denial rate per agent)
거부율이 높은 에이전트는 프로빙(probing)을 하거나, 혼란스럽거나, 고장난 상태일 수 있습니다. 각 케이스는 살펴볼 가치가 있습니다.
export async function highDenialAgents(thresholdPct = 20) {
const since = new Date(Date.now() - 24 * 3_600_000);
const logs = await theauth.audit.query({ since, limit: 5000 });
...
여기서는 기본 인수로 20 퍼센트를 하드코딩했습니다. 실제 코드를 작성할 때는 환경 변수에서 임계값(threshold)을 읽어오십시오. 왜냐하면 적절한 숫자는 트래픽에 따라 달라지기 때문입니다.
시간당 호출량(Call volume per hour)
제어되지 않는 루프는 단일 에이전트로부터의 호출 급증으로 나타납니다. 지난 한 시간을 계산하여 사용자가 정하는 상한선과 비교하십시오.
export async function callsLastHour(agentId: string): Promise<number> {
const recent = await theauth.audit.query({
agentId,
...
이 두 가지를 5분 또는 10분마다 스케줄러에서 실행하십시오. 크론 작업(cron job)으로 충분합니다. 이를 위해 스트리밍 파이프라인(streaming pipeline)이 필요하지 않습니다.
제가 anomalyCount를 신뢰하지 않는 이유
신뢰 점수(trust score)는 이유에 INSUFFICIENT_PERMISSIONS, privilege, 또는 escalation이 포함된 거부된 항목의 수를 계산합니다. 내장된 거부 이유는 "Agent 'x'가 'y'에 대해 '쓰기' 접근 권한을 부여하지 않음"과 같은 평문 문장입니다. 이 단어들은 나타나지 않으므로, 사용자가 직접 만든 후크(hook)나 래퍼(wrapper)가 이들 중 하나를 포함하는 이유를 작성하지 않는 한 anomalyCount는 0으로 유지됩니다.
거부율 계수와 마지막 위반 타임스탬프를 확인하려면 theauth.trust.computeScore(agent.id)로 점수를 읽으십시오. 다만, 감사관에게 anomalyCount를 권한 상승 탐지기(privilege escalation detector)로 제시해서는 안 됩니다. 레벨 작동 방식은 trust scoring page에서 설명합니다.
Step 6: 폴링 대신 알림 전송 (Push alerts instead of polling)
쿼리(Queries)는 무엇이 발생했는지 알려줍니다. 알림(Alerts)은 그것이 발생하는 동안 알려줍니다. theAuth는 세 가지 전달 경로를 제공하며, 이들은 별개의 메커니즘이며 각각의 이벤트 목록을 가집니다.
onViolation 후크 (The onViolation hook)
이 후크는 authorize()에 의해 반환되는 모든 거부에 대해 permission_denied, rate_limited, ip_blocked, time_restricted, 또는 approval_required와 같은 광범위한 유형으로 발생합니다. 라이브러리는 후크를 기다리지 않으며, 문서에는 하나 내부에서 예외(throw)가 발생하면 처리되지 않은 거부(unhandled rejection)가 된다고 경고하고 있습니다. 본문은 반드시 try/catch로 감싸야 합니다.
import { createTheAuth } from '@glinr/theauth';
import { createEventStreamModule } from '@glinr/theauth/auth';
...
이 후크는 생성되기 전의 라인에서 stream을 참조합니다. 이는 후크가 두 변수가 모두 존재하는 나중에 실행되기 때문에 작동하는 것입니다. 필요에 맞게 정리하십시오.
이벤트 스트리밍 페이지의 경고를 기억하십시오: 코어(core)는 절대 사용자를 위해 stream.emit()을 호출하지 않습니다. 모든 이벤트가 스트림에 도달하는 이유는 사용자의 코드가 그것을 방출했기 때문입니다. 여기서는 라이브러리가 결코 하지 않는 것임에도 불구하고, 우리가 직접 anomaly.detected를 방출합니다.
SSE 이벤트 스트림 (The SSE event stream)
이 스트림은 실시간 대시보드와 보안 도구에 적합합니다. 클라이언트는 베어러 토큰(bearer token)으로 연결하고 이름이 지정된 이벤트를 받습니다. 만약 클라이언트가 끊기면, since를 전달하여 theauth_stream_events 테이블에서 놓친 내용을 재실행할 수 있습니다.
여기에는 두 가지 날카로운 지점이 있습니다. 둘 다 문서에 나와 있습니다. 만약 requireAuth: true를 설정하고 validateToken을 건너뛰면, 해당 모듈은 비어있지 않은 모든 토큰을 허용합니다. 그리고 이벤트 ID가 UUID인 경우, 자동 브라우저 재연결(reconnect) 기능은 아무것도 재생하지 못하는데, 이는 해당 모듈이 Last-Event-ID 헤더를 날짜로 파싱하기 때문입니다. 마지막 타임스탬프는 직접 추적하여 since 매개변수를 전달해야 합니다. 자세한 내용은 이벤트 스트리밍 페이지에서 확인할 수 있습니다.
재생(Replay) 테이블은 감사(audit) 테이블과 분리되어 있습니다. 재생 기능은 감사 항목이 아닌 스트림 이벤트를 읽어오며, 호출당 최대 1000개를 최신순으로 반환합니다. 이를 시스템의 기록 저장소(system of record)로 오해해서는 안 됩니다.
영속적인 전송을 위한 웹훅 (Webhooks for durable delivery)
티케팅 도구 또는 SIEM 수집기처럼 외부 시스템이 이벤트를 필요로 할 때 웹훅을 사용해야 합니다. 각 엔드포인트는 URL, 비밀 키(secret), 그리고 명시적인 이벤트 목록을 가집니다.
const theauth = await createTheAuth({
database: { provider: 'sqlite', url: 'theauth.db' },
webhooks: [
...
동일한 규칙이 적용됩니다. 핵심 기능(core)에서 자동으로 이벤트를 방출해주지 않습니다. 사용자가 액션 후에 emit을 호출해야 합니다. 와일드카드(wildcard)는 존재하지 않으며, 이벤트 목록은 폐쇄적입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기