RBAC의 한계를 분석하고, ABAC과 ReBAC(관계 기반 접근 제어)의 개념, Google Zanzibar 모델, OpenFGA와 SpiceDB의 구현 방식을 비교합니다.
"이 사용자가 이 작업을 할 수 있는가?" 이 단순한 질문에 답하는 방식, 즉 인가(Authorization) 모델은 시스템의 복잡도에 따라 극적으로 달라집니다. 소규모 애플리케이션에서는 역할(Role) 기반 검사로 충분하지만, 수백만 객체와 복잡한 관계를 가진 시스템에서는 근본적으로 다른 접근이 필요합니다.
RBAC(Role-Based Access Control)는 가장 널리 사용되는 권한 모델입니다. 사용자에게 역할을 부여하고, 역할에 권한을 매핑하는 구조입니다.
// 기본 RBAC 모델
type Role = "admin" | "editor" | "viewer";
interface User {
id: string;
roles: Role[];
}
const rolePermissions: Record<Role, string[]> = {
admin: ["read", "write", "delete", "manage_users"],
editor: ["read", "write"],
viewer: ["read"],
};
function hasPermission(user: User, permission: string): boolean {
return user.roles.some((role) =>
rolePermissions[role]?.includes(permission)
);
}
// 사용 예시
function deletePost(user: User, postId: string): void {
if (!hasPermission(user, "delete")) {
throw new ForbiddenError("삭제 권한이 없습니다.");
}
// 삭제 로직 실행
}RBAC는 단순한 시나리오에서 효과적이지만, 요구사항이 복잡해지면 역할 폭발(Role Explosion)이라는 문제에 직면합니다.
예를 들어, 다음과 같은 요구사항이 있다고 가정합니다.
순수 RBAC로 이를 표현하면 3 x 4 x 3 = 36개의 역할이 필요합니다. 여기에 지역, 팀, 프로젝트 단위 권한이 추가되면 역할 수가 기하급수적으로 증가합니다.
// 역할 폭발 예시 - 실제로 이런 코드가 만들어집니다
type ExplodedRole =
| "sales_document_read"
| "sales_document_write"
| "sales_document_delete"
| "sales_project_read"
| "sales_project_write"
| "sales_project_delete"
| "dev_document_read"
| "dev_document_write"
// ... 36개의 역할이 필요
| "marketing_dashboard_delete";역할 폭발은 단순히 코드가 길어지는 문제가 아닙니다. 역할 관리가 불가능해지고, 새로운 리소스나 부서가 추가될 때마다 역할을 대량 생성해야 하며, 감사(Audit) 시 어떤 사용자가 실제로 어디에 접근할 수 있는지 파악하기 어렵습니다.
ABAC(Attribute-Based Access Control)는 역할 폭발 문제를 해결하기 위해 등장한 모델입니다. 사용자, 리소스, 환경의 속성(Attribute)을 기반으로 동적 정책을 평가합니다.
interface AbacContext {
subject: {
id: string;
department: string;
clearanceLevel: number;
location: string;
};
resource: {
type: string;
owner: string;
classification: "public" | "internal" | "confidential";
department: string;
};
action: string;
environment: {
time: Date;
ipAddress: string;
deviceType: string;
};
}
// ABAC 정책 예시
function evaluatePolicy(ctx: AbacContext): boolean {
// 정책 1: 같은 부서의 내부 문서만 수정 가능
if (ctx.action === "write" && ctx.resource.type === "document") {
if (ctx.resource.classification === "internal") {
return ctx.subject.department === ctx.resource.department;
}
}
// 정책 2: 기밀 문서는 clearance 3 이상만 접근
if (ctx.resource.classification === "confidential") {
return ctx.subject.clearanceLevel >= 3;
}
// 정책 3: 근무 시간 외에는 내부 자료 접근 불가
const hour = ctx.environment.time.getHours();
if (ctx.resource.classification !== "public") {
if (hour < 9 || hour > 18) {
return false;
}
}
return false;
}ABAC는 유연하지만, 정책이 코드 전반에 분산되고, 정책 간 충돌을 감지하기 어려우며, "이 사용자가 접근할 수 있는 리소스 목록"을 효율적으로 구하기 어렵다는 한계가 있습니다.
ReBAC(Relationship-Based Access Control)는 객체 간의 관계를 기반으로 권한을 결정하는 모델입니다. Google이 2019년 발표한 Zanzibar 논문에서 체계화되었으며, Google Drive, YouTube, Google Cloud IAM 등의 권한 시스템에 사용됩니다.
Zanzibar는 모든 권한을 관계 튜플(Relationship Tuple)로 표현합니다.
object#relation@subject
document:report#viewer@user:hong: "사용자 hong은 문서 report의 viewer이다"folder:shared#parent@document:report: "문서 report의 부모는 폴더 shared이다"team:dev#member@user:hong: "사용자 hong은 팀 dev의 member이다"model
schema 1.1
type user
type team
relations
define member: [user]
type folder
relations
define owner: [user]
define editor: [user, team#member]
define viewer: [user, team#member] or editor or owner
type document
relations
define parent: [folder]
define owner: [user]
define editor: [user, team#member] or owner
define viewer: [user, team#member] or editor or owner or viewer from parent이 모델에서 핵심적인 부분은 마지막 줄의 viewer from parent입니다. 이것은 "문서의 부모 폴더에서 viewer 권한을 상속받는다"는 의미입니다. 이 한 줄로 폴더 계층 구조의 권한 상속이 자동으로 처리됩니다.
위 그래프에서 user:kim이 document:report를 볼 수 있는지 확인하는 과정입니다.
document:report#viewer@user:kim 직접 관계? -- 없음document:report#editor@user:kim? -- 없음 (editor는 viewer를 포함)document:report#owner@user:kim? -- 없음viewer from parent 확인: document:report의 parent는 folder:sharedfolder:shared#viewer@user:kim? -- 직접은 없음folder:shared#viewer에 team:dev#member 포함, team:dev#member@user:kim? -- 있음결론: user:kim은 document:report의 viewer입니다.
OpenFGA는 Auth0 팀이 개발한 오픈소스 ReBAC 엔진으로, Zanzibar 모델을 구현합니다.
import { OpenFgaClient, CredentialsMethod } from "@openfga/sdk";
// OpenFGA 클라이언트 초기화
const fgaClient = new OpenFgaClient({
apiUrl: process.env.FGA_API_URL,
storeId: process.env.FGA_STORE_ID,
authorizationModelId: process.env.FGA_MODEL_ID,
credentials: {
method: CredentialsMethod.ApiToken,
config: {
token: process.env.FGA_API_TOKEN,
},
},
});
// 관계 튜플 작성
async function grantAccess() {
await fgaClient.write({
writes: [
// hong은 document:report의 editor
{
user: "user:hong",
relation: "editor",
object: "document:report",
},
// document:report의 부모는 folder:shared
{
user: "folder:shared",
relation: "parent",
object: "document:report",
},
// kim은 team:dev의 member
{
user: "user:kim",
relation: "member",
object: "team:dev",
},
// team:dev의 member는 folder:shared의 viewer
{
user: "team:dev#member",
relation: "viewer",
object: "folder:shared",
},
],
});
}
// 권한 확인 (Check)
async function canUserViewDocument(
userId: string,
documentId: string,
): Promise<boolean> {
const result = await fgaClient.check({
user: `user:${userId}`,
relation: "viewer",
object: `document:${documentId}`,
});
return result.allowed ?? false;
}
// 사용자가 접근 가능한 객체 목록 조회 (List Objects)
async function listAccessibleDocuments(
userId: string,
): Promise<string[]> {
const result = await fgaClient.listObjects({
user: `user:${userId}`,
relation: "viewer",
type: "document",
});
return result.objects;
}SpiceDB는 AuthZed가 개발한 또 다른 오픈소스 Zanzibar 구현입니다. OpenFGA와 비교하면 다음과 같습니다.
| 기준 | OpenFGA | SpiceDB |
|---|---|---|
| 개발사 | Auth0 (Okta) | AuthZed |
| 언어 | Go | Go |
| 스키마 언어 | JSON DSL | Zed 언어 |
| 저장소 | PostgreSQL, MySQL | PostgreSQL, CockroachDB, SpiceDB |
| Watch API | 지원 | 지원 |
| 일관성 모델 | Eventual | Configurable (Zookies) |
| gRPC | 지원 | 네이티브 |
definition user {}
definition team {
relation member: user
}
definition folder {
relation owner: user
relation editor: user | team#member
relation viewer: user | team#member
permission can_edit = editor + owner
permission can_view = viewer + can_edit
}
definition document {
relation parent: folder
relation owner: user
relation editor: user | team#member
permission can_edit = editor + owner
permission can_view = can_edit + parent->can_view
}권한 정책을 코드로 관리하는 Policy as Code 접근법은 인프라를 코드로 관리하는 IaC(Infrastructure as Code)와 같은 철학입니다.
OPA(Open Policy Agent)는 범용 정책 엔진으로, Rego 언어로 정책을 작성합니다.
package authz
import rego.v1
default allow := false
# 관리자는 모든 작업 허용
allow if {
input.user.roles[_] == "admin"
}
# 문서 소유자는 수정 가능
allow if {
input.action == "edit"
input.resource.type == "document"
input.resource.owner == input.user.id
}
# 같은 부서원은 읽기 가능
allow if {
input.action == "read"
input.resource.department == input.user.department
}
# 근무 시간 외 접근 제한
deny if {
not is_working_hours
input.resource.classification == "confidential"
}
is_working_hours if {
hour := time.clock(time.now_ns())[0]
hour >= 9
hour < 18
}Policy as Code의 가장 큰 이점은 버전 관리와 코드 리뷰입니다. 권한 정책의 변경 이력이 Git에 기록되고, Pull Request를 통해 리뷰와 승인 과정을 거칠 수 있습니다. 이는 규제 감사에서도 중요한 증거가 됩니다.
| 시나리오 | 추천 모델 | 이유 |
|---|---|---|
| 관리자/일반 사용자 구분만 필요 | RBAC | 단순하고 구현이 쉬움 |
| 사용자 속성에 따른 동적 정책 | ABAC | 속성 기반 유연한 정책 |
| 객체 간 관계와 상속이 중요 | ReBAC | 관계 그래프 기반 자연스러운 모델링 |
| 복잡한 비즈니스 규칙 | OPA + ABAC | 범용 정책 엔진의 유연성 |
| Google Drive 같은 공유 모델 | ReBAC | Zanzibar가 정확히 이 목적으로 설계됨 |
권한 모델은 RBAC의 단순함에서 시작하여, ABAC의 속성 기반 유연성을 거쳐, ReBAC의 관계 기반 모델링으로 진화해 왔습니다. 각 모델은 대체가 아니라 상호 보완적입니다. 실전에서는 RBAC로 기본 역할을 정의하고, ReBAC로 세밀한 리소스 권한을 관리하며, ABAC로 환경 조건을 추가하는 하이브리드 접근이 효과적입니다.
다음 장에서는 인증과 인가의 결과물인 토큰(Token)을 어떻게 안전하게 관리하고, 세션 전략을 어떻게 설계하는지 살펴봅니다.
이 글이 도움이 되셨나요?
Keycloak, Auth0, Zitadel의 아키텍처와 기능을 비교하고, 셀프 호스팅과 SaaS 간의 트레이드오프, 렐름/테넌트 설계, 페더레이션 전략을 정리합니다.
JWT의 클레임과 서명 알고리즘을 깊이 분석하고, 리프레시 토큰 로테이션, 토큰 저장 전략, 세션 관리 패턴, BFF 패턴을 실전 코드와 함께 설명합니다.
WebAuthn 프로토콜과 FIDO2 표준의 구조, 패스키의 등록 및 인증 플로우, 플랫폼 인증기와 크로스 디바이스 인증의 동작 원리를 설명합니다.