AI 에이전트를 위한 인증: 신원 및 범위 기반 권한
요약
본 글은 AI 에이전트와 인간 사용자를 위한 오픈소스 인증 시스템인 theAuth를 소개합니다. 이 시스템은 각 AI 에이전트에게 고유한 신원과 제한된 권한을 부여하여, API 키가 여러 곳에 분산되어 발생하는 보안 문제를 해결하는 것을 목표로 합니다. 이를 통해 에이전트는 생성, 호출 거부, 결정 기록 등 체계적인 관리가 가능해집니다.
핵심 포인트
- AI 에이전트 전용 오픈소스 인증 시스템 theAuth 소개
- 각 에이전트에 고유 신원 및 제한된 권한 부여가 핵심
- API 키 분산으로 인한 보안 위험을 해결하는 방안 제시
- 에이전트의 모든 행동은 결정 지점(decision point)을 거치도록 설계
theAuth는 AI 에이전트와 인간을 위한 오픈소스 인증 시스템입니다. GitHub에서 레포 저장하기 · 문서 읽기 · 빠른 시작하기 · theauth.dev
지난 봄, 저는 네 곳에서 하나의 API 키를 발견했습니다. 이 키는 크론 작업(cron job), Slack 봇, 코드 리뷰 에이전트, 그리고 팀원이 잊어버린 노트북에 있었습니다. 이 키는 한 인간에게 속한 것이었습니다. 그 키는 그 인간이 읽을 수 있는 모든 것을 읽을 수 있었고, 대부분의 내용을 쓸 수도 있었습니다.
당시까지는 아무 문제가 없었습니다. 그러다 제가 간단한 질문을 던졌습니다. 네 곳 중 어느 곳에서 지난 화요일에 삭제했는지 말해달라고요. 저는 대답할 수 없었습니다. 로그에는 그 인간이 그렇게 했다고만 나와 있었습니다.
이것이 이 가이드가 해결하는 문제입니다. 각 AI 에이전트에게 고유한 신원(identity)과 자체 토큰, 그리고 수행할 수 있는 항목의 짧은 목록을 부여하게 될 것입니다. 끝날 무렵에는 에이전트를 생성하고, 잘못된 호출을 거부하며, 모든 결정을 기록하고, 다른 에이전트에 영향을 주지 않으면서 하나의 에이전트만 비활성화하는 실행 가능한 스크립트를 갖게 될 것입니다.
저는 이 작업을 위해 open-source TypeScript 라이브러리인 theAuth (@glinr/theauth)를 사용합니다. 제가 일부를 작성했기 때문에 제 의견을 참고해 주시기 바랍니다. 적합하지 않은 부분은 명확히 밝힐 것입니다.
이것은 theAuth 가이드 중 8개 중 5번째입니다. 독립적으로 작동하므로 바로 시작할 수 있습니다. 이보다 앞서는 내용은 없습니다. 이는 가이드 6부터 8의 기반이 됩니다.
| 가이드 | 제목 | 언제 읽을까요? |
|---|---|---|
| 1 | 기존 Next.js 앱에 로그인 추가하기 | 아직 인증이 없는 앱을 가지고 있을 때 |
| ... | ||
| 건으로 사람들을 위한 것이라면? 가이드 1부터 시작하세요. AI 에이전트를 위한 것이라면? 가이드 5부터 시작하세요. 모든 가이드는 해당 개념에 관련된 문서 페이지로 연결됩니다. |
요약(TL;DR)
| 단계 | 수행할 작업 | 결과 |
|---|---|---|
| 1 | 인스턴스를 설치하고 생성합니다 | 에이전트가 활성화된 로컬 SQLite 데이터베이스 |
| ... |
필수 조건
Node 20 이상, TypeScript, 그리고 패키지 관리자가 필요합니다. 실행 중인 서버나 외부 데이터베이스는 필요하지 않습니다. 예제에서는 디스크의 SQLite를 사용하므로 나중에 파일을 열어볼 수 있습니다.
또한 한 가지 정직한 가정이 필요합니다: 시스템 내의 에이전트가 제어하는 도구를 호출한다는 것입니다. theAuth는 결정 지점(decision point)을 제공합니다. 코드는 행동하기 전에 반드시 이를 요청해야 합니다. 만약 에이전트가 자체 자격 증명으로 데이터베이스에 직접 접근할 수 있다면, 세상의 어떤 권한 목록도 그것을 막을 수 없습니다.
공유 키가 실패하는 이유
공유된 인간 키는 세 가지 문제를 가지고 있으며, 이 문제들은 누적됩니다.
첫째, 폭발 반경(blast radius)은 해당 인간의 권한과 같습니다. 풀 리퀘스트를 읽기만 필요한 코드 검토자가 모든 것에 대한 쓰기 접근 권한을 상속받습니다.
둘째, 에이전트 하나를 비활성화할 수 없습니다. 키를 교체하면 네 가지 프로세스가 한 번에 모두 작동을 멈춥니다. 기다리는 동안 노출된 키는 계속 살아있게 됩니다.
셋째, 감사 추적(audit trail)이 거짓말합니다. 모든 행은 인간의 이름을 명시합니다. 귀하의 규정 준수 질문과 자체 디버깅 과정이 벽에 부딪힙니다.
에이전트별 신원(per-agent identity)이 이 세 가지 문제를 모두 해결합니다. 각 에이전트는 하나의 행, 하나의 소유자, 그리고 하나의 권한 목록에 매핑되는 토큰을 보유합니다. 핵심 개념 페이지는 루프를 한 문장으로 설명합니다: 사용자가 에이전트를 생성하고, 에이전트가 행동하기 전에 authorize()를 호출하며, 모든 결정은 감사 추적에 기록됩니다.
초기 단계에서 중요한 구분이 하나 있습니다. 에이전트는 사용자가 아닙니다. 이메일도 없고, 비밀번호도 없고, 세션도 없고, OAuth 계정도 없습니다. 베어러 토큰(bearer token)과 권한 집합을 가집니다. 만약 에이전트의 비밀번호 재설정을 연결하는 자신을 발견한다면, 당신은 사용자를 원하는 것입니다. 에이전트 신원 페이지가 그 경계를 긋습니다.
단계 1: 인스턴스를 설치하고 생성합니다
mkdir agent-identity-demo && cd agent-identity-demo
npm init -y
npm install @glinr/theauth
...
이제 demo.ts를 만드세요. 제가 한 조각씩 구축할 것이며, 전체 파일은 아래 블록들의 합입니다.
import { createTheAuth } from '@glinr/theauth';
const theauth = await createTheAuth({
...
두 가지 설정에 대해 언급할 가치가 있습니다. auditAll은 모든 authorize() 호출을 기록하며, 기본값은 켜기(on)입니다. tokenExpiry는 expiresAt을 전달하지 않을 때 새 에이전트가 얼마나 오래 살아있게 할지 설정합니다. 24시간의 기본값은 여러분이 잊어버린 에이전트도 내일이면 작동을 멈춘다는 의미입니다. 저는 그 기본값이 마음에 듭니다. 잊힌 자격 증명(credential)은 남아돌기보다 만료되어야 합니다.
빠른 시작(quickstart) 가이드에서는 대신 스캐폴딩된 Next.js 버전을 원할 경우 동일한 설정을 안내합니다: https://docs.theauth.dev/quickstart.
2단계: 소유자 시드(Seed)하기
모든 에이전트는 소유자를 가지며, 이 소유자는 theauth_users에 행으로 존재해야 합니다. 해당 열은 외래 키(foreign key)입니다. 이 단계를 건너뛰면 첫 번째 생성 호출에서 FOREIGN KEY constraint failed 오류를 보게 될 것입니다. 누구나 한 번쯤 겪는 일입니다.
만약 이미 theAuth의 자체 인증 모듈을 실행하고 있다면, 가입(sign-up)이 여러분을 위해 해당 행을 생성합니다. 임시 스크립트에서는 다음처럼 하나를 삽입하세요:
import { users } from '@glinr/theauth';
theauth.db.insert(users).values({
...
이 삽입 패턴은 빠른 시작 문제 해결 섹션에서 가져온 것이며 SQLite에 작동합니다. Postgres의 경우, 데이터베이스 문서에 일치하는 호출 목록이 있습니다.
3단계: 작업별로 에이전트 생성하기
제가 따르는 규칙은 이렇습니다. 사람당 한 명이 아니라, 작업(job)당 하나의 에이전트를 만드세요. 야간 풀 리퀘스트 검토자(pr-reviewer)와 환불 봇(refund bot)은 같은 사람이 두 가지를 설정했더라도 절대로 동일한 신원(identity)을 공유해서는 안 됩니다.
const reviewer = await theauth.agent.create({
ownerId: 'user-1',
name: 'pr-reviewer',
...
토큰은 kv_로 시작하며, base64url로 인코딩된 32개의 무작위 바이트와 총 46자로 구성됩니다. theAuth는 SHA-256 해시만 저장합니다. 전체 데이터베이스 덤프로는 활성 토큰을 알아낼 수 없습니다.
그것에는 비용이 따릅니다. 평문(plaintext)은 생성 시 정확히 한 번만 볼 수 있습니다. 프로세스가 종료되기 전에 비밀 관리자(secrets manager)에 저장하거나, 새 값을 받기 위해 순환(rotate)시켜야 합니다. 나중에 복구할 수는 없습니다.
적절한 에이전트 유형 선택하기
type 필드는 세 가지 값을 가집니다.
autonomous(자율형): 승인 단계가 필요한 권한 제약(permission constraint)이 요구하는 경우가 아니라면 스스로 실행되는 에이전트입니다. Cron 작업이나 비감독 보조 도우미(unattended assistants)가 여기에 해당합니다. 이것이 기본 선택지입니다.delegated(위임형): 위임 체인(delegation chain)을 통해 다른 에이전트로부터 권한을 받는 에이전트입니다. 단일 작업을 위해 생성되는 단기 작업자(short-lived workers)에 사용합니다.service(서비스형): MCP 서버나 내부 마이크로서비스와 같은 인프라를 위한 장기간 유지되는 신원(identity)입니다. 서비스 계정(service account)처럼 취급하세요.
사용자당 제한 사항 유의하기
기본적으로 한 사용자는 10개의 활성 에이전트를 소유할 수 있습니다. 11번째 생성 호출은 User <id> has reached the maximum of <n> active agents.라는 메시지를 가진 일반적인 Error를 발생시킵니다. 이 오류는 전용 코드가 없으며, REST 엔드포인트에서는 500으로 보고됩니다. 플릿(fleet)을 운영하는 경우 초기화 시 maxPerUser를 높이고, 코드가 아닌 메시지로 오류를 처리하세요.
단계 4: 모든 작업 전에 확인하기
에이전트 신원은 사용자의 코드가 질문할 때까지 아무것도 하지 않습니다. 모든 민감한 작업 앞에 호출을 배치하세요.
const allowed = await theauth.authorize(reviewer.id, {
action: 'read',
resource: 'mcp:github:repos',
...
결과는 { allowed, reason?, auditId }입니다. 검토자(reviewer)는 read만 보유합니다. 동일한 리소스에 대한 write는 실패하며, 거부 사유와 함께 감사 로그(audit log)에 기록됩니다.
리소스 패턴 일치 방식
리소스는 콜론으로 구분된 문자열입니다. 원하는 규칙을 선택할 수 있습니다. mcp:github:repos, billing:refunds, 그리고 db:users:write 모두 작동합니다. 구문(syntax)보다 일관성(consistency)이 더 중요합니다.
와일드카드(*)는 사람들을 놀라게 하는 한 가지 동작 방식이 있습니다. * 세그먼트는 단일 세그먼트 와일드카드가 아닙니다. 매처(matcher)는 첫 번째 *에서 멈추고, 그 뒤에 오는 모든 것(아무것도 포함하여)을 허용합니다. 즉, mcp:github:*는 mcp:github:repos, mcp:github:repos:comments, 그리고 mcp:github 자체와 일치합니다.
*를 마지막 위치에만 두세요. mcp:*:repos와 같은 패턴은 "모든 서버의 리포지토리만"처럼 보이지만, mcp:slack:channels도 허용합니다. 와일드카드가 없다면, 패턴과 리소스는 동일한 수의 세그먼트를 가져야 합니다. 전체 표는 permissions page에서 확인할 수 있습니다.
Actions are free-form (액션은 자유 형식입니다)
액션 목록에는 고정된 세트가 없습니다. read, write, execute, delete가 일반적이지만, 로그에 기록하는 것이 더 자연스럽다면 comment나 refund 등을 정의할 수 있습니다. theAuth는 요청된 액션이 권한의 actions 배열에 나타나는지 확인합니다. 단어를 해석하지 않습니다.
Step 5: Add constraints (제약 조건 추가)
권한은 제약 조건을 가질 수 있으며, 모든 제약 조건은 통과해야 합니다. 바로 이 지점에서 최소 권한 원칙(least privilege)이 구체화됩니다. "파일 쓰기 가능"이라는 것은 "사무실 네트워크에서 시간당 20회, /tmp/agent/ 아래의 파일만 쓸 수 있음"으로 바뀝니다.
const filer = await theauth.agent.create({
ownerId: 'user-1',
name: 'file-writer',
...
각 제약 조건에는 알아야 할 경계(edges)가 있습니다.
Rate limits (속도 제한)
maxCallsPerHour는 롤링 시간 동안 에이전트 및 리소스별 호출 수를 5분 단위 버킷으로 계산합니다. 한도를 초과하는 호출은 Rate limit exceeded로 시작하는 사유를 갖게 됩니다. 감사 로그(audit log)에는 이들이 rate_limited가 아닌 denied로 기록됩니다. 비록 타입이 두 값 모두를 허용하더라도 그렇습니다.
Argument patterns (인수 패턴)
allowedArgPatterns는 정규 표현식 문자열을 사용합니다. arguments 내의 모든 문자열 값은 모든 패턴과 일치해야 합니다. 만약 대안으로 두 가지 패턴을 나열한다면, 아무것도 통과하지 못할 것입니다. 대신 하나의 패턴에 교대(alternation)를 사용하여 작성하세요. 이 검사는 비문자열 값을 건너뛰며, arguments가 누락된 경우 전체 요청을 건너뜁니다.
마지막 세부 사항이 중요합니다. 만약 도구 래퍼(tool wrapper)가 arguments를 전달하는 것을 잊는다면, 제약 조건은 조용히 아무것도 하지 않습니다. 매번 전달하세요.
시간 범위 (Time windows)
timeWindow는 UTC가 아닌 서버의 로컬 클럭과 HH:MM 문자열을 비교합니다. 자정(midnight)을 넘기는 시간대, 예를 들어 22:00부터 06:00까지는 지원되지 않습니다. 야간 근무 시간대가 필요하다면, 이를 다르게 모델링하거나 Auth 외부에서 강제해야 합니다.
IP 허용 목록 (IP allowlists)
ipAllowlist는 정확한 IPv4 주소와 IPv4 CIDR 범위를 사용합니다. IPv6는 지원되지 않습니다. 인증 요청 시 ip를 반드시 전달해야 합니다. ip가 없는 요청은 실패하며, 이는 안전한 기본값입니다.
승인 게이트 (Approval gates)
requireApproval: true로 설정하고 authorize()는 항상 This action requires human approval before execution이라는 이유로 거부합니다.
const deployer = await theauth.agent.create({
ownerId: 'user-1',
name: 'deploy-bot',
...
승인 문서를 주의 깊게 읽으세요. 이 부분이 사람들을 혼란스럽게 합니다. theAuth는 승인 UI를 제공하지 않으며, authorize()는 결코 승인을 조회하지 않습니다. 사람이 승인한 후에도 authorize()는 여전히 거부합니다. 애플리케이션이 theauth.approval.get(id)가 approved라고 말할 때 스스로 작업을 수행해야 합니다. 강제 지점은 theAuth입니다. 워크플로우는 사용자의 몫입니다. 승인 흐름 페이지에 요청, 승인 및 정리 호출이 있습니다.
단계 6: 가능하다면 템플릿부터 시작하세요
권한 배열을 수동으로 작성하는 것은 지겹습니다. theAuth는 이름이 지정된 템플릿을 일반 Permission[] 배열로 제공합니다.
import { permissionTemplates, getPermissionTemplate } from '@glinr/theauth';
const reader = await theauth.agent.create({
...
이름은 readonly, readwrite, admin, mcpBasic, mcpFull, rateLimitedRead, approvalRequired, 그리고 businessHours입니다. 습관 때문에 admin을 사용하지 마세요. 이는 모든 리소스의 모든 작업에 대한 권한을 부여하며, 이는 결국 공유된 인간 키(shared human key)와 같습니다.
한 가지 주의할 점이 있습니다. permissionTemplates에서 복사본을 전파하면 공유 객체에 대한 참조를 가집니다. 만약 나중에 항목을 변경하면 모든 사람의 템플릿이 바뀝니다. 편집할 계획이라면 항상 getPermissionTemplate을 사용하세요.
단계 7: HTTP 엣지에서 토큰 확인하기
지금까지는 에이전트 ID가 곧바로 authorize()로 들어갔습니다. 실제 시스템에서는 에이전트가 베어러 토큰(bearer token)과 함께 API를 호출합니다. 미들웨어에서 authorizeByToken을 사용하세요.
export async function handle(request: Request): Promise<Response> {
const token = request.headers.get('Authorization')?.replace('Bearer ', '');
if (!token) return new Response('Unauthorized', { status: 401 });
...
theAuth는 토큰을 해시하고, 조회하며, 권한을 평가하고, 결정을 기록합니다. JWT도 없고, 별도의 인증 서비스로 네트워크 왕복(network round trip)이 필요하지 않습니다. 호출은 자체 데이터베이스에 도달합니다.
두 진입점 간에는 두 가지 동작 방식의 차이가 있으며, 둘 다 프로덕션 환경에서 문제가 될 수 있습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기