Contents

Opinionated Zanzibar

Zanzibar

개요

Zanzibar(이하 잔지바)는 구글에서 실제 사용하고 있으며, 수년 전에 논문을 통해 공개된 권한 관리 시스템입니다. 잔지바는 주로 관계 튜플이란 것에 의해 권한 관계가 정의됩니다. 각각의 사용자, 권한 관계, 객체에 대해서 튜플 이용하여 아래와 같이 정의합니다.

document:roadmap#owner@user:alice
document:roadmap#viewer@user:bob
document:roadmap#viewer@group:platform#member
group:platform#member@user:charlie

읽는 것 자체는 쉽습니다.

  1. 로드맵 문서에 대해서 앨리스라는 유저가 오너이다.
  2. 로드맵 문서에 대해서 밥이란 유저라 뷰어이다.
  3. 로드맵 문서에 대해서 플랫폼 그룹의 멤버는 뷰어이다.
  4. 유저 찰리가 그룹 플랫폼의 멤버이다.

그리고 이 튜플들을 그래프 알고리즘으로 순회하여 로드맵 문서에 누가 데이터를 쓸 수 있는지, 누가 데이터를 읽을 수 있는 지 확인하여 권한을 확인합니다. 이 방식은 단순하고 강력합니다. 단지 규칙 조금을 추가함으로 새로운 유저나 객체, 권한 관계 등을 서술할 수 있습니다.

반대로

하지만 다르게 보면 표현력이 높아질수록 관계 정의와 튜플 조합의 가능한 경로도 함께 늘어납니다. OpenFGA를 비롯한 다양한 잔지바 구현체는 당연히 존재하고, authorization model과 relationship tuple을 함께 사용해 관계를 판정합니다. 문제는 표현할 수 없다는 점이 아니라, 운영자가 전체 권한 구조를 직관적으로 설명하기 어려워질 수 있다는 점입니다. 시각화나 시뮬레이터가 충분히 개발되어 있지 않는다면, 누구에게 어떤 권한이 있고, 어떤 객체에 접근하기 위한 권한은 어떤게 존재하고, 결과적으로 누가 어떤 권한으로 무엇에게 접근할 수 있는지를 확인하는 건 인간 관리자에게 어려운 문제가 됩니다.

그래서 저는 잔지바를 좀 더 형식적으로 만들 필요가 있다고 생각합니다.

Opinionated

개요

제가 제안하는 방향은 유저와 권한, 객체를 명시적으로 분리하는 것입니다. 이 모델은 잔지바의 일반적인 관계 표현력을 모두 유지하려는 모델이 아닙니다. 조직 권한 관리에서 자주 필요한 패턴만 남기고, 검증과 감사, 시각화를 쉽게 만들기 위한 제한형 ReBAC 모델에 가깝습니다.

전체 유저 그룹은 실제 조직 구성도처럼 구성되겠죠. 예를 들어, 개발1팀에 앨리스가 팀장, 밥이 팀원으로 존재합니다. 그럼 CTO -> 개발1팀 -> 앨리스, CTO -> 개발1팀 -> 밥, 이런 식으로 DAG가 구성될 것입니다. 팀장이라는 사실은 유저 포함 관계가 아니라, alice -> write -> Architecture 같은 별도의 권한 부여로 표현합니다. 그리고 권한은 기본적인 관리자 -> 쓰기 -> 읽기 권한 트리만 존재한다고 가정하겠습니다. 객체는 다양한 개발 관련 문서가 되겠죠. Project Titan -> Architecture -> Design1.md 가 있다고 가정합니다.

이렇게 그래프가 있을 때, 모든 개발1팀은 디자인1 문서에 대해 읽기 권한만 존재합니다. 앨리스는 팀장이기에 쓰기 권한을 별도로 부여했습니다.

사다리타기

위 가정에서 개발1팀에 부여된 read 권한만 그래프로 그리면

user DAG                  permission DAG          object DAG

CTO                       admin                   Project Titan
 |                         |                       |
 v                         v                       v
dev-1 --------|          write                  Architecture
 | \          |            |                       |
 |  \         |            v                       v
 |   bob      |----------read ------------------> Design1.md
 |
alice

이런 모습이 될 것입니다. 제가 제안하는 방식은 사다리타기와 유사합니다.

  1. 유저 DAG는 아래에서 위로 역방향으로 올라갈 수 있습니다.
  2. 권한 DAG는 위에서 아래로 순방향으로 내려갈 수 있습니다.
  3. 객체 DAG는 위에서 아래로 순방향으로 내려갈 수 있습니다.
  4. 각 DAG는 유저 -> 권한 -> 객체 방향으로만 순회할 수 있으며, 양방향 이동은 엄격히 금지됩니다.

그럼 Bob이 Design1.md 파일에 접근할 수 있는 방향을 이제 사다리타기처럼 그려볼 수 있을 것입니다.

  1. 먼저 bob -> dev-1로 올라갑니다.
  2. dev-1 -> read로 수평이동 합니다.
  3. read -> Design1.md로 수평이동합니다.

결과적으로 bob도, alice도, 그리고 추후 dev-1 아래에 새로 추가되는 팀원이나 부임하는 팀장도, 빠짐없이 Design1.md 파일에 접근할 수 있는 것입니다.

다른 경우인 alice의 쓰기 권한도 마찬가지입니다.

user DAG                  permission DAG          object DAG

CTO                       admin                   Project Titan
 |                         |                       |
 v                         v                       v
dev-1       |------------write ----------------->Architecture
 | \        |              |                       |
 |  \       |              v                       v
 |   bob    |             read                  Design1.md
 |          |
alice ------|

alice는 사실 dev-1으로 올라갈 필요없이 직접 alice -> write로 수평이동하고, write에서 Architecture로 다시 수평이동합니다. Architecture부터는 Design1.md로 그대로 내려오면 됩니다. 그럼 간단한 사다리타기를 통해 alice 또한 Design1.md 파일에 쓰기 권한이 있음을 알 수 있죠. 그리고 권한 DAG는 하위의 권한 또한 포함한다는 개념을 이용하면 읽기도 가능함을 체크할 수 있습니다.

예제 정리

지금까지의 예제에서 핵심은 간단합니다. 유저, 권한, 객체는 각각 독립적인 DAG로 존재합니다. 그리고 실제 권한 부여는 이 세 DAG를 잇는 사다리 줄 하나로 표현됩니다.

dev-1에 read 권한을 부여한 첫 번째 예제에서는 bob이 직접 권한을 가지고 있지 않습니다. 하지만 bob은 유저 DAG를 따라 dev-1까지 올라갈 수 있습니다. dev-1에는 read로 이어지는 사다리 줄이 있고, read는 다시 Design1.md로 이어집니다. 그래서 bob은 Design1.md를 읽을 수 있습니다. alice도 마찬가지입니다. alice 역시 dev-1 아래에 있기 때문에 같은 사다리 줄을 탈 수 있습니다.

두 번째 예제에서는 alice에게 write 권한을 별도로 부여했습니다. 이 경우 alice는 dev-1까지 올라갈 필요가 없습니다. alice에서 바로 write로 이동하고, write에서 Architecture로 이동한 뒤, 객체 DAG를 따라 Design1.md까지 내려갑니다. 따라서 alice는 Design1.md에 쓸 수 있습니다. 또한 권한 DAG에서 write는 read보다 상위 권한이므로, alice는 Design1.md를 읽을 수도 있습니다.

이렇게 보면 권한 검증은 복잡한 관계 전체를 해석하는 일이 아니라, 출발점에서 도착점까지 갈 수 있는 사다리 경로가 존재하는지 확인하는 일에 가까워집니다.

좀 더 이론적으로

조금 더 형식적으로 말하면, 이 모델은 세 개의 부분 순서 집합과 그 사이를 잇는 grant 관계로 볼 수 있습니다. 여기서 grant는 subject -> permissionpermission -> object라는 두 개의 독립 간선이 아닙니다. 실제 권한 부여는 아래와 같은 하나의 원자적인 3항 관계입니다.

grant(
  subject = dev-1,
  permission = read,
  object = Design1.md
)

사다리 그림에서 선이 두 번 꺾여 보이더라도, 저장되는 사실은 (subject, permission, object) 하나입니다. 이렇게 정의하지 않으면 같은 read 노드에 붙은 다른 객체와 원치 않는 조합이 생길 수 있습니다. 그래서 사다리 줄 하나는 반드시 하나의 grant로 취급되어야 합니다.

유저 DAG는 포함 관계입니다. 어떤 유저가 어떤 그룹에 속해 있다면, 권한 검증 시 유저는 자신의 상위 그룹으로 올라갈 수 있습니다. bob이 dev-1까지 올라갈 수 있는 이유가 이것입니다.

권한 DAG는 권한의 포함 관계입니다. admin은 write를 포함하고, write는 read를 포함합니다. 따라서 write 권한을 가진 사용자는 read 권한도 만족할 수 있습니다. 하지만 read에서 write로 올라갈 수는 없습니다. 권한 상승이 허용되면 검증 모델이 바로 깨지기 때문입니다.

객체 DAG는 객체의 포함 관계입니다. Project Titan 아래에 Architecture가 있고, Architecture 아래에 Design1.md가 있습니다. 상위 객체에 걸린 권한은 하위 객체로 내려갈 수 있습니다. 반대로 하위 객체의 권한이 상위 객체로 올라가지는 않습니다.

마지막으로 grant는 유저 DAG의 어떤 노드에서 권한 DAG의 어떤 노드로, 그리고 다시 객체 DAG의 어떤 노드로 이어지는 사다리 줄입니다. 검증은 이 사다리 줄을 찾는 과정입니다. 요청한 유저에서 시작해 상위 유저나 그룹으로 올라가고, 거기서 요청 권한을 만족하는 사다리 줄을 찾고, 그 줄이 도착한 객체에서 요청 객체까지 내려갈 수 있는지 확인합니다.

이를 판정식으로 쓰면 아래와 같습니다.

allow(u, requestedPermission, requestedObject)

iff exists (s, p, o) in Grant such that
  u <=U s
  p >=P requestedPermission
  o >=O requestedObject

여기서 u <=U s는 요청한 유저가 어떤 상위 주체나 그룹까지 올라갈 수 있음을 의미합니다. p >=P requestedPermission은 부여된 권한이 요청 권한을 포함한다는 뜻입니다. o >=O requestedObject는 권한이 부여된 객체가 요청 객체의 상위 범위라는 뜻입니다.

deny도 같은 방식으로 표현할 수 있습니다. grant와 대칭되는 원자적 3항 관계로 deny를 두면 됩니다.

deny(
  subject = bob,
  permission = read,
  object = Design1.md
)

다만 deny를 도입하는 순간 모델에는 충돌 해결 규칙이 필요합니다. 같은 요청에 대해 grant 경로와 deny 경로가 모두 존재할 수 있기 때문입니다. 가장 단순한 정책은 deny 우선입니다. 즉 아래 조건을 만족하는 deny 경로가 하나라도 있으면 접근을 거부하고, deny 경로가 없을 때만 grant 경로를 확인합니다.

blocked(u, requestedPermission, requestedObject)

iff exists (s, p, o) in Deny such that
  u <=U s
  p >=P requestedPermission
  o >=O requestedObject

따라서 최종 판정은 not blocked(...) and allow(...)가 됩니다. 이 규칙을 쓰면 특정 그룹에 넓게 권한을 열어두고, 일부 유저나 하위 객체만 차단하는 예외 정책도 표현할 수 있습니다. 대신 deny가 추가되는 만큼 설명 가능성은 조금 복잡해집니다. 관리자는 “왜 허용되는가"뿐 아니라 “왜 차단되는가"도 함께 추적해야 합니다.

그래서 무엇이 장점임?

가장 큰 장점은 검증 방향이 명확해진다는 점입니다. 기존 잔지바 모델에서는 튜플을 충분히 자유롭게 작성할 수 있는 대신, 특정 사용자가 특정 객체에 대해 특정 권한을 가지는지 확인하려면 관계 그래프 전체를 해석해야 합니다. 반면 이 방식에서는 탐색 방향이 제한됩니다.

유저 DAG에서는 아래에서 위로 올라갑니다. 권한 DAG와 객체 DAG에서는 위에서 아래로 내려갑니다. 그리고 각 DAG 사이의 이동은 오직 유저 -> 권한 -> 객체 방향의 사다리 줄을 통해서만 가능합니다. 따라서 권한 검증은 아래와 같은 질문으로 단순화됩니다.

  1. 요청한 유저에서 출발해 어떤 상위 유저 그룹까지 올라갈 수 있는가?
  2. 그 유저 또는 그룹에서 요청한 권한으로 이어지는 사다리 줄이 있는가?
  3. 해당 권한이 요청한 권한을 포함하는가?
  4. 사다리 줄이 도착한 객체에서 요청한 객체까지 내려갈 수 있는가?

이 조건을 만족하는 경로가 하나라도 존재하면 접근을 허용할 수 있습니다. 반대로 경로가 존재하지 않으면 거부하면 됩니다. 즉 권한 검증은 더 이상 임의의 관계 그래프를 헤매는 문제가 아니라, 방향이 정해진 세 개의 DAG와 그 사이를 잇는 사다리 줄을 찾는 문제가 됩니다.

이 구조는 관리자 입장에서도 직관적입니다. 권한을 부여한다는 것은 세 그래프를 새로 섞는 일이 아니라, 이미 존재하는 세 축 위에 하나의 사다리 줄을 추가하는 일입니다. 개발1팀 전체에 읽기 권한을 주고 싶다면 dev-1 -> read -> Design1.md를 추가하면 됩니다. 앨리스에게만 쓰기 권한을 주고 싶다면 alice -> write -> Architecture를 추가하면 됩니다.

권한 회수도 같은 방식으로 단순해집니다. 특정 사다리 줄을 제거하면 그 줄을 통해 만들어지던 접근 경로가 사라집니다. 유저 조직도, 권한 체계, 객체 구조를 직접 수정하지 않아도 됩니다. 각 그래프의 역할이 분리되어 있으므로 변경의 영향 범위도 비교적 쉽게 예상할 수 있습니다.

시각화하기도 좋습니다. 유저, 권한, 객체가 한 그래프 안에 뒤섞여 있으면 화면에 보여줄 때도 관계의 의미를 계속 해석해야 합니다. 반면 이 방식에서는 세 개의 DAG를 세로 축으로 고정하고, 실제 권한 부여만 사다리 줄로 그리면 됩니다. 관리자는 특정 유저나 그룹에서 출발해 어떤 권한 줄을 타고 어떤 객체까지 도달하는지 눈으로 따라갈 수 있습니다. 권한이 과하게 열려 있는지, 특정 객체에 접근하는 경로가 어디서 생겼는지도 비교적 쉽게 확인할 수 있습니다.

그래프 DB와도 잘 맞습니다. 예를 들어 Neo4j의 Cypher 같은 쿼리 언어를 사용한다면, “유저 DAG를 역방향으로 올라간 뒤, grant 관계를 통해 권한으로 이동하고, 객체 DAG를 순방향으로 내려간다"는 검증 규칙을 쿼리로 표현할 수 있습니다. 즉 권한 검증 로직을 애플리케이션 코드 안에 복잡하게 숨기기보다, 그래프 탐색 쿼리로 명시적으로 드러낼 수 있습니다. 시뮬레이터나 관리자 도구를 만들 때도 같은 쿼리를 이용해 “왜 이 사용자가 이 객체에 접근 가능한가"를 경로 단위로 보여줄 수 있습니다.

물론 이 방식은 잔지바보다 표현이 제한적입니다. 아무 관계나 튜플로 자유롭게 만들 수는 없습니다. 대신 그 제한 덕분에 사람이 읽을 수 있는 구조가 됩니다. 유저, 권한, 객체가 섞이지 않고, 권한 부여는 항상 사다리 줄 하나로 표현됩니다. 저는 권한 시스템에서 이 정도의 제약은 오히려 장점이라고 생각합니다.

무엇을 포기하는가?

이 모델은 모든 권한 정책을 표현하려는 모델이 아닙니다. grant와 deny는 표현할 수 있지만, 둘 다 (subject, permission, object) 형태의 명시적인 사다리 줄로 제한됩니다. 이 제한 덕분에 검증과 시각화가 쉬워지지만, 반대로 런타임 조건을 많이 요구하는 정책에는 잘 맞지 않습니다.

예를 들어 시간 제한 권한, 근무 시간 조건, IP나 기기 신뢰도, MFA 상태 같은 ABAC 조건은 이 모델의 기본 관심사가 아닙니다. “작성자”, “결재자”, “문서 소유자"처럼 런타임 속성에서 계산되는 관계도 별도의 동기화나 materialization 과정 없이는 바로 표현하기 어렵습니다. 서로 배타적인 권한을 강제하는 separation of duties, 교차 테넌트 경계, 하나의 객체가 여러 부모를 가질 때 생기는 상속 충돌도 별도 정책 계층에서 다루는 편이 낫습니다.

즉 이 모델의 목표는 모든 정책을 흡수하는 것이 아닙니다. 조직, 권한, 객체 계층이 비교적 안정적이고, 권한 부여와 차단을 명시적인 사다리 줄로 관리할 수 있는 환경에서 운영 가능성을 높이는 것이 목표입니다.

결론

잔지바는 강력하고 범용적인 권한 모델입니다. 하지만 범용적인 만큼, 운영하는 사람 입장에서는 관계가 어디서 생기고 어디로 흘러가는지 파악하기 어려워질 수 있습니다. 특히 권한 관계가 많아질수록 튜플 하나하나는 읽기 쉬워도, 전체 권한 구조는 점점 직관에서 멀어집니다.

제가 제안한 방식은 잔지바를 더 제한적인 형태로 바라보자는 것입니다. 유저, 권한, 객체를 각각 분리된 DAG로 두고, 실제 권한 부여와 차단은 그 사이를 잇는 사다리 줄로만 표현합니다. 이 방식은 모든 권한 모델을 표현할 수는 없겠지만, 대신 사람이 읽고, 시각화하고, 쿼리하고, 설명하기 쉬운 구조를 제공합니다.

권한 시스템에서 중요한 것은 단순히 “가능한가"를 빠르게 계산하는 것만은 아닙니다. “왜 가능한가"를 설명할 수 있어야 하고, “어디를 바꾸면 사라지는가"를 예측할 수 있어야 합니다. 그런 점에서 이 opinionated한 사다리 모델은 충분히 실용적인 대안이 될 수 있다고 생각합니다.