확장 가능한 권한 관리를 위한 모델

확장 가능한 권한 관리를 위한 모델

서비스를 처음 설계할 때 권한 관리는 비교적 단순하게 시작됩니다.

사용자에게 역할을 부여하고, 그 역할에 따라 접근 가능한 기능을 정의하는 방식은 직관적이며 빠르게 적용할 수 있기 때문입니다. 이러한 이유로 대부분의 시스템은 RBAC(Role-Based Access Control)에서 출발하게 됩니다.

그러나 서비스가 성장하고 기능이 다양해질수록, 권한에 대한 요구사항은 점점 더 복잡해집니다. 단순히 “이 사용자가 이 기능을 사용할 수 있는가”를 판단하는 수준을 넘어, “이 사용자가 이 리소스를 지금 이 상황에서 접근할 수 있는가”라는 질문으로 바뀌게 됩니다.

예를 들어, 특정 문서만 선택적으로 공유해야 하는 경우가 있습니다. 또는 같은 조직에 속해 있더라도 리소스의 상태에 따라 접근이 제한되어야 하는 상황도 발생합니다. 더 나아가, 시간이나 위치와 같은 외부 환경에 따라 권한이 달라지는 요구도 자연스럽게 등장합니다.

이러한 요구들이 쌓이기 시작하면, RBAC만으로는 더 이상 충분하지 않다는 사실을 점차 체감하게 됩니다.

RBAC, 가장 직관적이지만 점점 복잡해지는 구조

RBAC는 역할을 중심으로 권한을 관리하는 모델입니다.

사용자는 하나 이상의 역할을 가지며, 각 역할에는 수행 가능한 권한이 정의되어 있습니다. 이 구조는 조직 단위의 접근 제어에 잘 맞으며, 초기 시스템이나 조직 기반 권한이 중심인 서비스에서 특히 효율적입니다. 구조를 단순하게 도식화하면 아래와 같습니다.

이 다이어그램에서 핵심은 권한이 사용자에게 직접 부여되지 않고, 역할을 통해 간접적으로 연결된다는 점입니다. 사용자는 역할을 할당받고, 시스템은 해당 역할에 매핑된 권한 집합을 기준으로 접근 가능 여부를 판단합니다.

RBAC는 리소스 단위의 세밀한 제어를 표현하기 어렵습니다. 특정 문서 하나만 공유하거나, 특정 사용자에게만 제한적으로 접근을 허용하는 요구는 역할 기반 모델로는 자연스럽게 표현되지 않습니다. 이런 요구를 RBAC 안에 계속 밀어 넣으면 역할 수가 빠르게 증가하고, 결국 역할 폭증(role explosion)으로 이어지기 쉽습니다.

무엇보다 중요한 문제는, RBAC는 관계를 표현하지 못한다는 점입니다. 사용자와 리소스 사이의 연결, 혹은 리소스 간의 계층 구조와 같은 개념은 역할만으로는 설명되지 않습니다.

이러한 한계를 보완하기 위해 권한 모델은 다음 단계로 확장됩니다.

ABAC, 상황을 이해하는 권한 모델

RBAC가 정적인 권한 모델이라면, ABAC는 동적인 권한 모델이라고 볼 수 있습니다.

ABAC는 사용자, 리소스, 그리고 환경의 다양한 속성을 기반으로 권한을 판단합니다. 즉, 권한은 더 이상 “누가 어떤 역할을 가지고 있는가”만으로 결정되지 않고, “현재 어떤 상태이며 어떤 조건에 있는가”까지 포함하여 판단됩니다. 이러한 방식은 현실적인 서비스 요구사항을 반영하는 데 매우 효과적입니다.

ABAC를 이해할 때 가장 중요한 요소는 보통 다음 네 가지입니다.

  • Subject: 행위를 시도하는 주체입니다. 예: 사용자, 시스템 계정, 서비스 계정
  • Resource: 접근 대상입니다. 예: 문서, 예약, 결제, 프로젝트
  • Action: 수행하려는 동작입니다. 예: read, write, approve, cancel
  • Context: 판단에 영향을 주는 주변 조건입니다. 예: 시간, 위치, 접속 채널, 리소스 상태, 사용자 상태

그리고 이 조건들을 모두 평가한 뒤 최종적으로 allow 또는 deny라는 권한 판단이 결정됩니다.

Netflix와 같은 글로벌 서비스에서는 이러한 특성이 매우 중요하게 작용합니다. 콘텐츠는 국가별로 라이선스가 다르기 때문에, 사용자의 지역에 따라 접근 가능 여부가 달라집니다. 동일한 사용자라도 접속 위치가 바뀌면 권한이 달라지는 구조입니다. 이 경우 권한은 역할이 아니라, “사용자의 위치”와 “콘텐츠의 허용 범위”라는 속성의 조합으로 결정됩니다.

Airbnb의 공개 Help Center 문서를 보면 예약 확정 여부에 따라 노출 정보와 메시지 가능 범위가 달라집니다. 예약이 확정되면 게스트는 호스트의 연락처, 숙소 주소, 도착 가이드를 확인할 수 있고, 예약과 연결된 메시지 스레드에서 호스트와 소통할 수 있습니다. 반대로 이런 정보와 기능은 예약 확정 전에는 제한됩니다.[A1]

예를 들어 OPA의 Rego로 표현하면 다음과 같은 형태가 됩니다.[O1]

package airbnb.messaging

default allow = false

allow if {
    input.action == "view_host_contact"
    input.reservation.status == "confirmed"
    input.actor.id == input.reservation.guest_id
}

allow if {
    input.action == "view_listing_address"
    input.reservation.status == "confirmed"
    input.actor.id == input.reservation.guest_id
}

allow if {
    input.action == "message_reservation_thread"
    input.reservation.status == "confirmed"
    input.actor.id in {
        input.reservation.guest_id,
        input.reservation.host_id,
    }
}

allow if {
    input.action == "send_media_in_message"
    input.reservation.status == "confirmed"
    input.actor.id in {
        input.reservation.guest_id,
        input.reservation.host_id,
    }
}

이 예시에서는 사용자의 역할보다도, 예약 상태가 confirmed인지와 사용자가 해당 예약의 참여자인지가 더 중요합니다. 즉 권한 판단의 핵심이 역할이 아니라 예약 상태와 사용자-예약 맥락 조합에 있다는 점에서, Airbnb의 예약 흐름은 ABAC의 대표적인 예시로 볼 수 있습니다.

이처럼 ABAC는 현실 세계의 조건과 상태를 권한 모델에 반영할 수 있게 해줍니다.

그러나 ABAC만으로도 여전히 해결되지 않는 문제가 존재합니다.

ReBAC, 관계를 통해 권한을 이해

서비스의 성격에 따라 권한은 단순한 속성의 문제가 아니라 관계의 문제가 됩니다.

예를 들어, 한 문서가 특정 사용자에게 공유되었는지, 또는 특정 폴더에 포함되어 있는지에 따라 접근 가능 여부가 달라지는 경우를 생각해볼 수 있습니다. 이러한 요구는 단순한 조건이나 역할이 아니라, “연결된 관계”를 기반으로 판단해야 합니다.

ReBAC는 바로 이 지점을 해결하기 위한 모델입니다.

ReBAC에서는 권한을 관계 그래프로 표현합니다. 사용자와 리소스, 그리고 리소스 간의 연결을 정의하고, 이 관계를 따라가며 권한을 판단합니다. 즉 권한은 고정된 값이 아니라 그래프 탐색 결과로 계산됩니다.

ReBAC를 실제 시스템으로 구현할 때는 이러한 관계를 보통 튜플(tuple) 단위로 저장합니다. SpiceDB 계열 모델에서는 이를 관계 데이터로 저장하고 계산하며, 기본 형식은 다음과 같습니다.[R1][R2]

        resource       subject
           ID           type
         \ˍˍˍˍ_ˍ\       \ˍ__ˍ\
 resource:doc-000#editor@group:team-001#member
/¯¯¯¯¯¯¯/        /¯¯¯¯¯/      /¯¯¯¯¯¯¯//¯¯¯¯¯/
resource         relation     subject  subject
  type                          ID    relation

각 요소를 풀어보면 다음과 같습니다.

  • Resource: 보호 대상 리소스입니다. 예: resource:proj-alpha
  • Relation: 리소스와 주체 사이의 관계 이름입니다. 예: viewer, editor, parent
  • Subject: 관계의 주체입니다. 일반적으로 사용자이지만, 다른 리소스가 될 수도 있습니다.

예: actor:3q__NT_b_4-1

예를 들어 아래와 같이 표현할 수 있습니다. Actor는 작업 공간 내에서 사용자로 식별되는 모델입니다.

resource:proj-alpha#viewer@actor:3q__NT_b_4-1
resource:docu-launch-plan#editor@actor:4e__NT_b_4-1
resource:docu-launch-plan#parent@resource:proj-alpha
resource:proj-alpha#owner@actor:3g__NT_b_4-1

이 튜플을 그래프로 그리면 아래와 같습니다.

이 튜플들은 각각 다음 의미를 가집니다.

  • 가은은 proj-alpha 리소스를 조회할 수 있습니다.
  • 나연은 docu-launch-plan 문서를 수정할 수 있습니다.
  • docu-launch-plan 문서는 proj-alpha 리소스에 속해 있습니다.
  • 다영은 proj-alpha 리소스의 소유자입니다.

여기서 parent는 권한 자체가 아니라 리소스 간의 포함 관계를 나타냅니다. 실제 권한 전파는 SpiceDB 스키마에서 parent->read, parent->write 같은 permission 정의를 통해 계산됩니다.[R2]

즉 ReBAC에서는 권한을 코드 안의 조건문으로만 풀어내지 않고, 이런 관계 데이터를 그래프처럼 쌓아 둔 뒤 질의 시점에 계산합니다. 그래서 공유, 상속, 하위 리소스 전파 같은 요구를 훨씬 자연스럽게 표현할 수 있습니다.

Google Drive에서 문서를 공유하는 과정을 떠올려보면, 특정 사용자에게 직접 권한을 부여할 수도 있고, 폴더 단위로 공유할 수도 있습니다. 폴더에 권한이 부여되면, 그 하위 문서에도 동일한 권한이 자연스럽게 전파됩니다.

이 구조는 역할 기반으로는 표현하기 어렵고, 조건 기반으로도 제한적입니다. 하지만 관계 기반 모델에서는 매우 자연스럽게 표현됩니다. 문서와 폴더, 사용자 간의 관계를 정의하고, 그 관계를 따라가며 권한을 계산하면 되기 때문입니다.

결국 ReBAC는 “권한을 관계 데이터 그래프로 정의하고 계산하는 방식”이라고 볼 수 있습니다. 다만 그만큼 그래프 모델링과 조회 비용도 함께 관리해야 합니다.

모델에 따른 선택 기준

Model RBAC ABAC ReBAC
잘 맞는 문제 기능 단위 접근 제어, 조직/역할 기반 권한 상태, 시간, 위치, 채널, 승인 흐름 기반 동적 권한 공유, 상속, 폴더 구조, 협업 관계 기반 권한
핵심 판단 기준 사용자가 어떤 역할을 가졌는가 현재 어떤 속성과 조건을 만족하는가 누구와 무엇이 어떤 관계로 연결되어 있는가
대표 예시 관리자만 사용자 관리 가능 예약 확정 이후에만 메시지 가능 공유된 문서와 하위 폴더 접근

여기까지 살펴보면 하나의 결론에 도달하게 됩니다.

  • RBAC는 여전히 필요합니다. 역할별 유스케이스로 자연스럽게 표현되는 기본 접근 제어는 가장 단순하고 빠르게 처리할 수 있습니다.
  • ABAC는 현실적인 조건과 대상의 상태를 동적으로 반영하는 데 필수적입니다.
  • ReBAC는 공유와 관계, 그리고 권한 전파를 표현하는 데 반드시 필요합니다.

이 세 가지는 서로를 대체하는 모델이 아니라, 서로를 보완하는 모델입니다.

플랫폼 통합

플랫폼에서는 이 세 가지 모델을 분리된 솔루션처럼 다루지 않습니다. Guard라는 단일한 서비스와 공통 모듈의 권한 진입점을 두고, 그 아래에서 Application, OPA, SpiceDB를 책임에 맞게 조합합니다. 핵심은 기술을 나누는 것이 아니라 책임을 나누는 것입니다.

  • RBAC는 Application 내부에서 Role별 Feature 접근 가능 여부를 평가합니다.
  • OPA는 상태와 조건 기반 정책을 평가합니다.
  • SpiceDB는 관계 기반 권한을 평가합니다.
  • Guard 및 Authorizer 모듈은 이 판단들을 조합하고, 최종 결과와 감사 로그를 관리합니다.

플랫폼의 기본 원칙은 default deny입니다. OPA는 default 규칙을 통해 명시적으로 허용되지 않은 요청을 기본 거부로 다룰 수 있고[O2], SpiceDB 역시 관계나 permission 계산 결과가 성립하지 않으면 권한이 부여되지 않는 방식으로 동작합니다.[R1][R2] 플랫폼에서는 이 두 계층을 함께 사용하며, 최종 허용은 두 계층이 모두 명시적으로 허용할 때만 성립합니다. 즉 OPA와 SpiceDB의 최종 결합 조건은 AND입니다.

권한 오케스트레이션

Feature Authorizer는 요청 처리 코드에서 직접 OPA나 SpiceDB를 각각 호출하지 않도록 만드는 계층입니다. 각 서비스 코드에서는 can actor do action on resource? 형태로 Feature Authorizer에 질의하고, Authorizer는 내부적으로 어떤 정책 엔진을 어떤 순서로 호출할지 결정합니다. 공통 모듈인 Guard에서는 사용성을 높인 추상화된 형태의 라이브러리를 제공합니다.

평가 흐름

  1. Application 내부 Feature 계층에서 현재 actor가 해당 기능에 접근 가능한 역할인지 RBAC로 먼저 확인합니다.
  2. Feature Authorizer가 권한 판단 요청을 수신하고, 공통 포맷의 입력으로 정규화합니다.
  3. OPA가 리소스 상태, 사용자 속성, 요청 맥락을 기준으로 정책을 평가합니다.
  4. SpiceDB가 actor와 resource 사이의 관계 및 상위 리소스 전파를 기준으로 권한을 평가합니다.
  5. OPA와 SpiceDB가 모두 allow일 때만 최종 허용하고, 하나라도 deny면 최종 결과는 deny입니다.
  6. Guard가 각 단계의 판단 근거와 최종 결과를 감사 로그로 기록합니다.

책임 분리 아키텍처

OPA는 서비스별 정책 분리 구현

반면 OPA는 공통화 비중이 훨씬 낮습니다.

현실적으로 서비스마다 상태 모델과 정책 조건이 다르기 때문에, Rego 정책을 완전히 공통화하기는 어렵습니다.

예를 들어 같은 write 액션이라도 어떤 서비스는 status == "draft"일 때만 허용하고, 다른 서비스는 approval == "approved" 이후에는 수정을 금지할 수 있습니다. 이런 조건은 관계 그래프보다 도메인 규칙에 가깝고, 애플리케이션 코드에 흩어져 있던 판단을 정책 파일로 분리하는 것에 더 가깝습니다.

그래서 플랫폼에서는 OPA 정책을 공통과 서비스로 구분하고 번들 공유 형태로 구현하는 방식을 가져갑니다. 빠른 응답 속도를 위해 각 서비스 옆에 사이드카 형태로 붙여 배포하는 방식을 사용합니다.

package lime.document

default allow = false

allow if {
  input.action == "write"
  input.resource.status == "draft"
  input.actor.active == true
}

allow if {
  input.action == "publish"
  input.resource.status == "reviewed"
  input.actor.role == "approver"
}

이렇게 하면 각 서비스는 자기 도메인 상태에 맞는 정책을 독립적으로 배포할 수 있고, 네트워크 홉으로 생겨나는 지연을 최소화할 수 있습니다.

SpiceDB는 확장 가능한 튜플 구조

플랫폼에서는 SpiceDB의 튜플 구조와 스키마를 서비스마다 새로 갈아엎지 않도록, 다양한 도메인 구조를 담을 수 있는 형태로 설계합니다.

즉 folder, document만을 위한 고정 모델이 아니라, 서비스별로 달라지는 resource, subject, relation 구조를 확장 가능한 스키마로 가져갑니다.

예를 들어 어떤 서비스는 panel, board, post, comment 구조를 가질 수 있고, 다른 서비스는 project, scrum, backlog 구조를 가질 수 있습니다. 공통 인터페이스를 유지하고, 각 서비스는 자신에게 필요한 리소스 타입과 관계만 확장해 사용합니다.

definition actor {}

definition group {
  relation member: actor | group#membership
  relation parent: group

  permission membership = member + parent->membership
}

definition resource {
  relation creator: actor | group#membership
  relation manager: actor | group#membership
  relation editor: actor | group#membership
  relation viewer: actor | group#membership
  relation parent: resource

  permission read = creator + manager + editor + viewer + parent->read
  permission write = creator + manager + editor + parent->write
  permission manage = creator + manager + parent->manage
  permission share = creator + manager + parent->share
}

이 스키마의 핵심은 subject를 actor와 group 두 축으로 받아들이고, group 안에 다시 group을 포함할 수 있게 만들어 공유 권한 트리를 구성할 수 있다는 점입니다.

이 구조의 장점은 명확합니다.

  • 서비스마다 다른 리소스 계층을 담을 수 있습니다.
  • actor 직접 공유와 group 기반 공유를 함께 표현할 수 있습니다.
  • 그룹 트리와 상위 리소스 전파를 공통 패턴으로 가져갈 수 있습니다.
  • 개별 서비스가 관계 저장 방식 자체를 다시 설계할 필요가 없습니다.

그리고 SpiceDB를 사용하는 모듈은 애플리케이션 안에 위치하지 않고, 별도의 서비스로 실행합니다. 이것이 Guard와 함께 플랫폼 권한 계층의 한 축을 이룹니다. 별도 서비스로 분리하면 관계 그래프 저장소와 체크 API를 독립적으로 운영할 수 있고, 여러 서비스가 동일한 관계 모델을 공유하기도 쉬워집니다.

최종 권한의 판단 근거

OPA와 SpiceDB는 서로 다른 종류의 판단을 내립니다.

  • OPA는 입력 데이터와 정책 규칙을 근거로 판단합니다.
  • SpiceDB는 관계 그래프를 근거로 판단합니다.

따라서 둘 중 하나만 봐서는 “왜 허용되었는지” 또는 “왜 거부되었는지”를 완전히 설명하기 어렵습니다. 실제 운영에서는 단순한 allow/deny보다, 그 판단의 근거를 추적하는 일이 중요해집니다.

그래서 플랫폼에서는 최종 권한 판단의 근거를 하나의 감사 로그 저장소로 모읍니다. Guard는 OPA 결과와 SpiceDB 결과를 각각 수집한 뒤, Transaction별로 통합된 최종 결론과 함께 저장합니다.

{
  "stageId": "NT:b:4-1",
  "actorId": "4e@NT:b:4-1",
  "action": "write",
  "resource": "resource:docu-launch-plan",
  "result": "DENY",
  "rebac": {
    "check": "resource#editor@actor",
    "result": "ALLOW",
    "reason": ["resource.editor"]
  },
  "abac": {
    "policy": "lime.document.allow",
    "result": "DENY",
    "reason": ["resource.status=archived"]
  },
  "finalReason": "relationship는 충족했지만 상태 정책에서 거부됨"
}

이렇게 하면 운영자는 단순히 “권한이 없다”는 결과만 보는 것이 아니라, 관계는 통과했는지, 상태 조건에서 막혔는지, 어떤 규칙이 최종 거부를 만들었는지까지 추적할 수 있습니다.

결국 Guard의 역할은 관계 정책과 조건 정책을 연결하고, 서비스별 확장성을 유지하면서도, 최종 판단 근거를 일관된 방식으로 수집하고 분석하는 것입니다.

마무리

권한 관리는 단순한 기능이 아니라 서비스의 구조를 결정하는 핵심 요소입니다. RBAC만으로 출발할 수는 있지만, 서비스가 성장하고 권한 요구가 복잡해질수록 조건과 관계를 함께 다루는 구조로 확장해야 합니다.

결국 현실적인 권한 모델은 Role, Condition, Relationship 세 가지 요소를 분리해 설계하고, 이를 다시 하나의 흐름으로 통합하는 것이 확장 가능한 권한 아키텍처의 핵심입니다.

drine

참고 자료