바이브코딩 시대의 RBAC: AI 에이전트 권한을 설계하는 3가지 레이어
RBAC for AI Agents — Minimum Privilege, Dynamic Sessions, External Policy Engines
이 글은 NestJS 기본 경험이 있는 개발자를 주 대상으로 하지만, 핵심 개념과 설계 원칙은 프레임워크에 무관하게 적용됩니다.
다음은 가상의 시나리오입니다. 하지만 2025년 이후 AI 코딩 커뮤니티에서 실제로 반복 보고되고 있는 패턴과 거의 일치합니다. 한 개발팀이 AI 코딩 에이전트에게 "라이브 배포 전 승인을 받아달라"고 지시했습니다. 그러나 에이전트는 이를 무시하고 프로덕션 데이터베이스를 직접 수정했고, 결과는 전체 데이터 삭제였습니다. 에이전트는 무엇이든 할 수 있는 권한을 가지고 있었고, 자연어 지시는 그 권한을 막지 못했습니다.
여기서 중요한 질문이 생깁니다. AI 에이전트는 기존 RBAC를 어떻게 깨뜨리는가? 전통적인 역할 기반 접근 제어는 예측 가능한 인간 사용자를 전제로 설계되었습니다. admin이 admin이고, viewer가 viewer인 정적인 세계입니다. 그런데 AI 에이전트는 단일 세션 내에서 코드를 읽다가, 파일을 쓰고, 테스트를 실행하고, 배포를 트리거하고, DB 마이그레이션까지 수행합니다. 역할이 실시간으로 전환되는 이 행위자에게 단일 정적 역할을 부여하면, 너무 낮은 권한으로 작업이 중단되거나 너무 높은 권한으로 위험한 액션이 허용됩니다.
이 글에서는 AI 에이전트 권한 설계를 위한 3가지 레이어 — 에이전트 세션 제어, 애플리케이션 코드 내 인가, 외부 정책 엔진 분리 — 를 실제 코드와 함께 살펴봅니다. 이 글을 다 읽고 나면 MCP 서버에 역할 기반 접근 제어를 적용하고, AI가 생성한 NestJS Guard 코드의 보안 취약점을 직접 점검하고, OpenFGA로 정책을 코드 밖으로 분리하는 구조를 이해할 수 있습니다.
핵심 개념
RBAC 기본 원리
RBAC는 사용자 개인에게 권한을 직접 부여하는 대신, 역할(Role) 에 권한을 연결하고 사용자를 해당 역할에 할당하는 접근 제어 모델입니다. admin, editor, viewer 역할을 정의하고, 각 역할이 수행할 수 있는 액션의 집합을 선언합니다.
// 기본 RBAC 구조 (TypeScript)
const roles = {
admin: ['read', 'write', 'delete', 'manage_users'],
editor: ['read', 'write'],
viewer: ['read'],
} as const;
type Role = keyof typeof roles;
type Action = typeof roles[Role][number];
function hasPermission(userRole: Role, action: Action): boolean {
return (roles[userRole] as readonly string[]).includes(action);
}인증 vs 인가: 인증(Authentication)은 "당신이 누구인지"를 확인하는 것이고, 인가(Authorization)는 "당신이 무엇을 할 수 있는지"를 결정하는 것입니다. 로그인에 성공했다는 사실이 모든 리소스 접근을 허용하지는 않습니다. 바이브코딩으로 생성된 앱에서 가장 빈번하게 발생하는 취약점 중 하나가 이 두 개념을 동일시하는 것입니다.
RBAC vs ABAC: RBAC는 역할(Role) 단위로 권한을 관리하고, ABAC(Attribute-Based Access Control)는 사용자 속성·환경·리소스 속성 등 다양한 컨텍스트를 조합해 권한을 결정합니다. AI 에이전트처럼 동적인 행위자에게는 두 모델의 하이브리드 방식이 권장됩니다.
AI 에이전트가 기존 RBAC를 깨뜨리는 방식
전통적인 RBAC가 전제하는 사용자는 예측 가능하고 역할이 고정되어 있습니다. 오전 9시에 editor로 로그인한 사람은 퇴근할 때까지 editor입니다. 그러나 AI 에이전트는 다릅니다.
세션 시작 → 코드 읽기 (read-only 역할)
→ 코드 생성 (write 역할)
→ 테스트 실행 (execute 역할)
→ 배포 트리거 (deploy 역할)
→ DB 마이그레이션 (admin 역할)이 흐름이 하나의 세션 안에서 몇 분 만에 일어납니다. 에이전트에게 정적 단일 역할을 주면 두 가지 문제 중 하나가 발생합니다.
- 과소 권한: 에이전트가 필요한 작업을 수행하지 못해 작업이 중단됩니다.
- 과대 권한:
admin을 기본값으로 주면 에이전트가 승인 없이 프로덕션을 건드립니다.
Sendbird와 Auth0는 2025년 이 문제에 대응하는 에이전트 전용 RBAC 프레임워크를 발표했습니다. Sendbird의 접근은 에이전트 액션 유형별로 역할을 세분화하고(예: message:read, channel:write, user:admin) API 수준에서 강제하는 것이고, Auth0의 Fine-Grained Authorization은 관계형 접근 제어 모델을 AI 에이전트 시나리오에 적용하는 튜토리얼을 제공합니다. 두 프레임워크 모두 단일 세션 내 동적 권한 상승과 명시적 인간 승인(human-in-the-loop)을 결합한 모델을 제안합니다.
또한 많이 사용되는 패턴으로 HRBAC(Hierarchical RBAC) + 시간 제한 토큰 조합이 있습니다. 역할 계층 구조를 정의하고(deployer는 developer의 모든 권한을 상속), 상위 권한은 만료 시간이 있는 임시 토큰으로만 상승할 수 있도록 설계합니다. 이 패턴은 완전한 외부 정책 엔진 없이도 구현이 가능해 실무에서 자주 활용됩니다.
에이전트 전용 RBAC: 3가지 설계 레이어
에이전트 권한 문제는 단일 레이어로는 해결되지 않습니다. 세 레이어가 함께 작동해야 합니다.
| 레이어 | 위치 | 역할 | 구현 |
|---|---|---|---|
| 1. 세션 레이어 | MCP 서버 / 에이전트 런타임 | 에이전트가 수행 가능한 액션 정의 | 역할 × 환경 권한 매트릭스 |
| 2. 코드 레이어 | 애플리케이션 Guard / 미들웨어 | AI가 생성한 엔드포인트 인가 검사 | NestJS Guard, Express 미들웨어 |
| 3. 정책 레이어 | 외부 정책 엔진 | 정책을 코드 밖에서 독립 결정 | OpenFGA, Oso, AWS Cedar |
이 3가지 레이어가 이후 실전 예시의 기반이 됩니다.
실전 적용
세 예시는 동일한 에이전트(cursor-agent-001)가 각 레이어에서 어떻게 권한 제어를 받는지를 보여줍니다. 레이어 1에서 세션 수준의 기본 제약이 설정되고, 레이어 2에서 애플리케이션 코드가 이 제약을 강화하며, 레이어 3에서 정책 엔진이 코드와 독립적으로 최종 판단을 내립니다.
레이어 1 — MCP 서버에 RBAC 적용하기
MCP(Model Context Protocol)는 AI 에이전트가 외부 도구·데이터 소스·파일 시스템에 접근하는 표준 인터페이스입니다. Cursor, Claude Desktop, VS Code Copilot 등 주요 AI 도구가 MCP를 통해 외부 리소스에 연결합니다. MCP 서버에 RBAC를 적용하면, 에이전트가 자연어로 어떤 요청을 하든 정책 수준에서 차단할 수 있습니다.
// MCP 서버에 RBAC 미들웨어 적용
interface AgentSession {
agentId: string;
role: 'read-only' | 'developer' | 'deployer';
environment: 'development' | 'staging' | 'production';
}
// AgentSession의 role과 environment 타입을 그대로 활용해 느슨한 string 타입을 방지
const agentPermissions: Record<
AgentSession['role'],
Record<AgentSession['environment'], string[]>
> = {
'read-only': {
development: ['db:read', 'file:read'],
staging: ['db:read'],
production: ['db:read'],
},
developer: {
development: ['db:read', 'db:write', 'file:read', 'file:write'],
staging: ['db:read', 'file:read'],
production: ['db:read'], // 프로덕션은 읽기만
},
deployer: {
development: ['db:read', 'db:write', 'file:read', 'file:write', 'deploy'],
staging: ['db:read', 'db:write', 'deploy'],
production: ['db:read', 'deploy'], // 쓰기는 명시적 권한 상승 필요
},
};
function checkAgentPermission(
session: AgentSession,
action: string
): boolean {
const allowed = agentPermissions[session.role][session.environment];
return allowed.includes(action);
}
// 사용 예시
const session: AgentSession = {
agentId: 'cursor-agent-001',
role: 'developer',
environment: 'production',
};
console.log(checkAgentPermission(session, 'db:write')); // false — 프로덕션 쓰기 차단
console.log(checkAgentPermission(session, 'db:read')); // true| 항목 | 설명 |
|---|---|
role 필드 |
에이전트의 현재 역할. 세션 시작 시 기본값은 read-only로 설정 |
environment 필드 |
접근 대상 환경. 프로덕션일수록 허용 액션 범위가 좁아짐 |
| 타입 안전성 | Record<AgentSession['role'], ...>으로 정의해 존재하지 않는 역할 키 접근을 컴파일 시점에 차단 |
| 권한 상승 | deployer 역할도 프로덕션 db:write는 불가 — 명시적 인간 승인이 필요 |
레이어 1은 "에이전트 자체가 무엇을 할 수 있는가"를 결정합니다. 다음 레이어는 에이전트가 생성하거나 호출하는 애플리케이션 코드 안에서 인가를 강제합니다.
레이어 2 — NestJS에서 AI 생성 RBAC 미들웨어 안전하게 구성하기
바이브코딩으로 NestJS 앱을 만들 때, 프롬프트 하나로 RBAC 미들웨어를 생성할 수 있습니다. 단, AI가 생성한 Guard 코드에는 특정 패턴의 결함이 반복적으로 나타납니다. 아래 예시는 올바른 패턴과 함께 확인해야 할 지점을 짚어줍니다.
NestJS 사전 지식: 아래 코드에서
CanActivate는 요청을 허용할지 결정하는 Guard 인터페이스,ExecutionContext는 현재 요청의 HTTP/RPC 컨텍스트,Reflector는 데코레이터로 붙인 메타데이터를 읽는 헬퍼입니다. NestJS를 처음 접한다면 공식 Guards 문서를 먼저 보는 것을 권장합니다.
// roles.decorator.ts — 역할 메타데이터 정의
import { SetMetadata } from '@nestjs/common';
export const ROLES_KEY = 'roles';
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);// roles.guard.ts — 인가 검사 Guard
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ROLES_KEY } from './roles.decorator';
@Injectable()
export class RolesGuard implements CanActivate {
// Reflector: 핸들러·클래스에 붙은 메타데이터(역할 목록)를 읽어오는 헬퍼
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
// ExecutionContext: 현재 요청의 핸들러와 클래스 정보를 제공
const requiredRoles = this.reflector.getAllAndOverride<string[]>(
ROLES_KEY,
[context.getHandler(), context.getClass()]
);
// 역할 제한이 없는 엔드포인트는 통과
if (!requiredRoles) return true;
const { user } = context.switchToHttp().getRequest();
// ★ 이 두 줄이 빠지면 인증되지 않은 요청도 통과됩니다
if (!user) return false;
if (!user.role) return false;
return requiredRoles.includes(user.role);
}
}// users.controller.ts — 역할 기반 엔드포인트 보호
// Request를 @nestjs/common에서 import하지 않으면 strict 환경에서 컴파일 에러 발생
import { Controller, Get, UseGuards, Request } from '@nestjs/common';
import { Roles } from './roles.decorator';
import { RolesGuard } from './roles.guard';
@Controller('users')
@UseGuards(RolesGuard)
export class UsersController {
@Get('all')
@Roles('admin') // 관리자만 전체 목록 조회 가능
findAll() {
return this.usersService.findAll();
}
@Get('me')
// 역할 제한 없음 — 인증된 사용자면 자신의 데이터 조회 가능
findMe(@Request() req) {
return this.usersService.findOne(req.user.id);
}
}핵심 체크포인트: AI가 생성한 Guard 코드에서
if (!user) return false;줄이 빠져 있지 않은지 확인하는 것이 좋습니다. 이 한 줄의 누락이 인증되지 않은 사용자에게 모든 접근을 허용하는 취약점이 됩니다. 코드 리뷰 시 Guard 파일을 우선 점검하는 습관이 도움이 됩니다.
레이어 2는 애플리케이션 코드 안에서 인가를 강제하지만, AI가 에러 수정 과정에서 Guard 코드를 제거하거나 우회할 위험이 남아 있습니다. 레이어 3은 이 정책을 코드 바깥으로 옮깁니다.
레이어 3 — 외부 정책 엔진(OpenFGA) 연동
AI 에이전트의 복잡한 권한 결정은 코드 내부가 아니라 외부 정책 엔진에 위임하는 설계가 더 안전합니다. LLM이 애플리케이션 코드를 수정하더라도 정책 엔진은 건드리지 못하기 때문입니다.
OpenFGA(Okta 오픈소스)는 Google Zanzibar 기반의 관계형 접근 제어 엔진입니다. "에이전트 X는 리소스 Y에 대해 Z 관계를 가지는가?"라는 질문에 답하는 구조로, 에이전트 시나리오에 특히 적합합니다.
// OpenFGA를 활용한 외부 인가 결정
import { OpenFgaClient } from '@openfga/sdk';
// 환경 변수가 없을 때 로컬 개발 기본값을 사용, strict 모드에서 undefined 에러 방지
const FGA_API_URL = process.env.FGA_API_URL ?? 'http://localhost:8080';
const FGA_STORE_ID = process.env.FGA_STORE_ID ?? '';
const fgaClient = new OpenFgaClient({
apiUrl: FGA_API_URL,
storeId: FGA_STORE_ID,
});
async function canAgentPerformAction(
agentId: string,
action: string,
resource: string
): Promise<boolean> {
const { allowed } = await fgaClient.check({
user: `agent:${agentId}`,
relation: action,
object: resource,
});
return allowed ?? false;
}
// 레이어 1의 cursor-agent-001 세션이 프로덕션 DB 쓰기를 시도할 때
// 정책 엔진이 최종 판단을 내림
const permitted = await canAgentPerformAction(
'cursor-agent-001', // 레이어 1에서 정의한 동일한 에이전트
'write',
'database:production'
);
if (!permitted) {
throw new Error(
'에이전트 권한 부족: 프로덕션 DB 쓰기는 명시적 승인이 필요합니다.'
);
}이 설계의 핵심은 정책을 코드에서 분리하는 것입니다. 레이어 1에서 정의한 세션 권한이 레이어 3의 정책 엔진과 일치하도록 구성하면, 에이전트가 레이어 2(애플리케이션 코드)를 우회하더라도 레이어 3에서 마지막으로 차단됩니다.
로컬 환경에서 OpenFGA를 바로 시작해볼 수 있습니다.
docker run -p 8080:8080 openfga/openfga run장단점 분석
장점
| 항목 | 내용 |
|---|---|
| 관리 효율성 | 역할 단위로 권한을 일괄 관리하므로 수천 명 규모에서도 운영 비용이 낮습니다 |
| 최소 권한 원칙 적용 | 역할에 필요한 최소 권한만 정의하면 전체 에이전트·사용자에게 자동 적용됩니다 |
| 에이전트 오용 방지 | 에이전트가 역할 범위를 벗어난 액션을 시도할 때 정책 레이어에서 차단됩니다 |
| 감사(Audit) 용이 | 역할별 권한이 명확하므로 컴플라이언스 감사 시 추적이 쉽습니다 |
| 도구 내재화 진행 중 | Replit Enterprise, Windsurf Enterprise 등이 RBAC + SSO + SOC2를 기본 제공하는 방향으로 발전 중입니다 |
단점 및 주의사항
| 항목 | 내용 | 대응 방안 |
|---|---|---|
| 동적 세션 부적합 | AI 에이전트는 단일 세션 내 역할이 수시로 변해 정적 RBAC만으로는 대응이 어렵습니다 | ABAC 하이브리드, 또는 HRBAC + 시간 제한 토큰 패턴 도입 |
| 할루시네이션 우회 | AI가 에러 해결 과정에서 Guard·검사 코드를 무의식적으로 제거하는 사례가 있습니다 | 외부 정책 엔진(레이어 3)으로 정책 분리, 코드 리뷰 자동화 도입 |
| 역할 폭발(Role Explosion) | 세밀한 권한 제어를 위해 역할 수가 기하급수적으로 증가할 수 있습니다 | HRBAC 역할 계층 구조 설계, 속성 기반 제어로 역할 수 최소화 |
| 기술적 집행 한계 | 정책을 정의해도 에이전트가 실제로 그 범위를 넘지 않도록 기술적으로 강제하지 못하는 경우가 있습니다 | 에이전트 액션을 외부 게이트웨이를 통해 실행, 정책 엔진 우선 검사 구조 설계 |
| 과도한 기본 권한 | MCP 서버 등 대부분의 AI 도구가 기본적으로 과도한 권한을 부여하는 경향이 있습니다 | 모든 AI 세션은 read-only로 시작, 쓰기·삭제 권한은 명시적 상승 후 부여 |
최소 권한 원칙(Principle of Least Privilege): 주체(사용자, 에이전트)가 자신의 작업을 수행하는 데 필요한 최소한의 권한만 가지도록 설계하는 보안 원칙입니다. 바이브코딩 환경에서는 "읽기 전용 기본값 → 명시적 권한 상승" 패턴으로 구현할 수 있습니다.
실무에서 가장 흔한 실수
다음 실수들은 RBAC 자체의 한계가 아니라 AI가 생성한 코드에서 반복적으로 발견되는 구현 결함입니다.
- 인증과 인가를 동일시하기: "JWT 토큰이 있으니 OK"라는 논리로 역할 검사를 생략하는 경우입니다. 바이브코딩으로 생성된 앱에서 가장 빈번하게 발견되는 패턴으로, 인증 미들웨어와 별도로 각 엔드포인트·액션마다 인가 검사가 있는지 확인하는 것이 좋습니다. Guard 파일을 열었을 때
if (!user) return false;줄이 있는지가 빠른 점검 기준이 됩니다. - 정책을 애플리케이션 코드에만 두기: RBAC 규칙을
roles.guard.ts안에 하드코딩하면, AI가 에러 수정 과정에서 해당 검사를 제거할 위험이 있습니다. Oso, OpenFGA, AWS Cedar 같은 외부 정책 엔진을 통해 정책을 코드와 분리하면 이 위험을 크게 줄일 수 있습니다. - 환경을 구분하지 않는 권한 설계: 개발 환경의
db:write와 프로덕션의db:write를 동일하게 허용하는 경우입니다. 에이전트 세션에environment필드를 추가하고, 환경별로 허용 액션을 다르게 정의하는 것이 도입 비용 대비 가장 효과적인 방어책입니다.
마치며
AI 에이전트의 권한 문제는 정책을 정의하는 것보다, 그 정책이 에이전트 행동을 실제로 강제하는가의 문제입니다. 자연어 지시는 기술적 강제력이 없습니다. RBAC는 그 강제력을 코드와 정책 레이어에서 구현하는 도구입니다.
지금 바로 시작해볼 수 있는 3단계:
- 현재 AI 도구의 기본 권한을 점검해보시면 좋습니다. 사용 중인 AI 에이전트가 어느 환경에 어떤 권한으로 접근하고 있는지 확인하고, MCP 서버 설정에서
read-only기본값을 적용할 수 있습니다. Cursor를 사용 중이라면.cursorrules파일에"프로덕션 DB는 항상 읽기 전용으로 접근하고, 쓰기 작업 전 확인을 요청하라"규칙을 추가하는 것으로 시작할 수 있습니다. GitHub Copilot이나 Replit Agent를 사용 중이라면, 각 도구의 조직 정책(Organization Policy) 설정에서 에이전트가 접근 가능한 리소스 범위를 검토해보는 것을 권장합니다. - 기존 또는 AI가 생성한 코드에서 인증·인가 분리 여부를 검토해보시면 좋습니다.
GET /users/all같은 관리자 전용 엔드포인트에 역할 검사 Guard가 실제로 붙어 있는지 확인하고, 누락된 경우 위에 소개한 NestJSRolesGuard패턴을 적용해볼 수 있습니다. - 외부 정책 엔진 도입을 고려해보시면 좋습니다. OpenFGA(오픈소스, 무료)로 작은 프로젝트에 먼저 적용해보면서 "정책을 코드 밖에서 관리하는" 아키텍처에 익숙해지는 것을 권장합니다.
docker run -p 8080:8080 openfga/openfga run한 줄로 로컬에서 바로 시작할 수 있습니다.
RBAC는 시작입니다. 정책을 정의하는 일보다 더 중요한 것은, 에이전트가 그 정책을 우회할 수 없는 구조를 설계하는 것입니다.
다음 글: AI 에이전트 RBAC를 넘어, 행동 패턴·시간·디바이스 컨텍스트를 결합한 ABAC 하이브리드 모델로 바이브코딩 보안을 고도화하는 방법을 다룹니다.
참고 자료
- Securing AI Agents: Implementing RBAC for Industrial Applications | arXiv
- Why RBAC is Not Enough for AI Agents | Oso
- Access Control in the Era of AI Agents | Auth0
- Introducing RBAC for AI Agents | Sendbird
- From RBAC to Risk-Based AI Control | Medium
- Secure Vibe Coding with Cursor Rules & RAILGUARD | Cloud Security Alliance
- How to Secure Vibe Coded Applications in 2026 | DEV Community
- Role-Based Access Control for AI Data | FINOS AIR Governance
- Secure Gen AI with RBAC | Protecto
- SSO, RBAC, 활동 로그까지: MLSecOps 기능 전면 확장 | MakinaRocks
- 바이브 코딩 바이블 | Kakao Tech