LabHub

블로그

Keycloak Authorization Services — UMA 2.0 기반 세밀한 권한 제어

한국어English日本語

들어가며

"이 사용자가 admin role을 갖고 있는가?"라는 질문만으로 권한을 결정할 수 있는 시스템은 생각보다 빨리 한계에 부딪힙니다. "이 사용자가 이 문서수정할 수 있는가? 단, 문서 소유자이거나, 같은 부서의 관리자이거나, 업무 시간 내의 요청일 때만"이라는 식의 요구가 들어오는 순간, role 기반 분기문은 애플리케이션 코드 전체로 흩어지기 시작합니다.

2026년 현재 이 문제의식은 더 절실해졌습니다. AI 에이전트가 사용자를 대신해 API를 호출하는 시대(Keycloak 26.6은 MCP authorization server 역할을 위한 OAuth Client ID Metadata Document를 실험적으로 지원합니다)에는 "누가"뿐 아니라 "무엇이, 어떤 위임 범위로, 어떤 리소스에" 접근하는지를 중앙에서 평가하고 감사할 수 있어야 합니다. 이 글에서는 Keycloak Authorization Services의 모델과 UMA 2.0 흐름을 해부하고, 실전 policy enforcer 설정, 성능 고려사항, 그리고 OPA/OpenFGA 같은 외부 인가 엔진과의 관계까지 다룹니다.

RBAC의 한계, 그리고 ABAC/ReBAC

먼저 인가 모델의 스펙트럼을 정리해 봅시다.

모델결정 기준예시한계
RBAC사용자에게 부여된 roleadmin은 모든 문서 수정 가능리소스 단위 구분 불가, role 폭발
ABAC주체/리소스/환경의 속성같은 부서 + 업무 시간이면 허용정책 작성/디버깅 난이도
ReBAC주체와 리소스 사이의 관계 그래프문서의 owner 또는 폴더 editor면 허용관계 데이터 동기화 비용

RBAC의 전형적인 실패 양상은 role 폭발(role explosion)입니다. "프로젝트 A의 편집자", "프로젝트 B의 열람자"를 모두 role로 만들면 프로젝트 수 × 권한 수만큼 role이 늘어나고, 결국 role 관리 자체가 새로운 권한 문제가 됩니다. 그렇다고 애플리케이션 코드에 if 분기로 풀면 정책이 코드 곳곳에 흩어져 감사도 변경도 어려워집니다.

여기서 필요한 것이 정책의 중앙화와 외부화(policy externalization)입니다. Keycloak Authorization Services는 OAuth 2.0 위에서 리소스 단위의 세밀한 인가를 중앙에서 평가하는 프레임워크로, RBAC을 포함해 ABAC, 시간 기반, 그리고 제한적인 ReBAC 스타일까지 표현할 수 있습니다.

Keycloak Authorization Services 아키텍처

4개의 빌딩 블록

Keycloak의 인가 모델은 네 가지 개념으로 구성됩니다. client 설정에서 Authorization Enabled를 켜면 해당 client가 "resource server"가 되고, 그 아래에 다음을 정의합니다.

+------------------------------------------------------------+
|                Resource Server (client)                    |
|                                                            |
|  +-----------+     +---------+                             |
|  | Resource  |---->| Scope   |   "무엇을" (document:123)    |
|  | (문서,API) |     | (view,  |   "어떤 행위" (view, edit)   |
|  +-----------+     |  edit)  |                             |
|        ^           +---------+                             |
|        |                ^                                  |
|  +-----+----------------+-----+                            |
|  |       Permission           |  "리소스/스코프와 정책을 연결" |
|  | (resource-based /          |                            |
|  |  scope-based)              |                            |
|  +-------------+--------------+                            |
|                |                                           |
|  +-------------v--------------+                            |
|  |          Policy            |  "누가/어떤 조건에" (role,   |
|  | (role, user, group, time,  |   group, time, regex ...)  |
|  |  regex, aggregated ...)    |                            |
|  +----------------------------+                            |
+------------------------------------------------------------+

이 분리가 중요한 이유는 정책의 재사용성 때문입니다. "업무 시간에만"이라는 time policy를 한 번 만들면 수십 개의 permission에서 재사용할 수 있습니다.

Policy 유형 정리

Policy 유형평가 기준활용 예
Rolerealm/client role 보유 여부관리자 전용 기능
User특정 사용자 지정시스템 계정 예외
Group그룹 멤버십 (계층 포함 가능)부서 단위 접근
Client요청한 client 식별내부 서비스 전용 API
Time시각/요일/기간 조건업무 시간 제한, 캠페인 기간
Regex토큰 클레임에 대한 정규식 매칭이메일 도메인, 속성 패턴
Client Scopeclient scope 보유 여부동의 기반 접근
Aggregated여러 정책의 조합복합 조건
JavaScriptJS 코드로 임의 로직 (deprecated 흐름)레거시 커스텀 로직

JavaScript policy에 대한 주의가 필요합니다. 과거에는 Admin Console에서 JS 코드를 직접 입력해 정책을 만들 수 있었지만, 보안상의 이유로 이 기능은 기본 비활성화되었고 배포 JAR을 통해서만 제한적으로 사용할 수 있는 deprecated 경로가 되었습니다. Nashorn 엔진 제거 이후의 흐름까지 고려하면, 신규 설계에서 JS policy에 의존하는 것은 피해야 합니다. 복잡한 커스텀 로직이 필요하다면 (1) 기존 정책 유형의 조합(aggregated policy)으로 풀거나, (2) Policy SPI로 Java 기반 커스텀 정책을 구현하거나, (3) 뒤에서 다룰 외부 인가 엔진과의 병행을 검토하세요.

PEP/PDP 모델

Authorization Services는 고전적인 XACML 용어의 구조를 따릅니다.

   +----------------+      (1) 요청       +---------------------+
   |    Client      +-------------------->+   Application       |
   | (브라우저/앱)    |                     |   = PEP             |
   +----------------+                     | (Policy Enforcement |
                                          |       Point)        |
                                          +----------+----------+
                                                     | (2) 결정 요청
                                                     |  (token / ticket)
                                          +----------v----------+
                                          |     Keycloak        |
                                          |   = PDP + PAP       |
                                          | (Policy Decision /  |
                                          |  Administration)    |
                                          +----------+----------+
                                                     | (3) Permit/Deny
                                                     |  (RPT 발급)
                                          +----------v----------+
                                          |   보호된 리소스       |
                                          +---------------------+

UMA 2.0 Grant 흐름 해부

UMA(User-Managed Access) 2.0은 OAuth 2.0의 확장으로, "리소스 소유자가 자신의 리소스에 대한 접근 정책을 관리하고, 클라이언트는 권한 티켓을 통해 인가를 협상한다"는 모델입니다. Keycloak에서의 흐름을 단계별로 보겠습니다.

 Client                Resource Server (PEP)            Keycloak (PDP)
   |                          |                              |
   | (1) GET /api/doc/123     |                              |
   |  (access token, RPT없음) |                              |
   +------------------------->|                              |
   |                          | (2) permission ticket 요청    |
   |                          +----------------------------->|
   |                          |    POST /authz/protection/   |
   |                          |         permission           |
   |                          |<-----------------------------+
   | (3) 401 + WWW-Authenticate: UMA                         |
   |     (as_uri, ticket)     |                              |
   |<-------------------------+                              |
   |                          |                              |
   | (4) POST /token                                         |
   |     grant_type=uma-ticket, ticket=...                   |
   +-------------------------------------------------------->|
   |                          |        (5) 정책 평가          |
   | (6) RPT (권한 내장 토큰)   |                              |
   |<--------------------------------------------------------+
   |                          |                              |
   | (7) GET /api/doc/123 (RPT)                              |
   +------------------------->| (8) RPT 검증 후 응답          |
   |<-------------------------+                              |

Permission Ticket과 RPT

두 가지 토큰 개념이 등장합니다.

실제 HTTP 요청으로 보면 다음과 같습니다.

POST /realms/myrealm/protocol/openid-connect/token HTTP/1.1
Host: sso.example.com
Authorization: Bearer ACCESS_TOKEN
Content-Type: application/x-www-form-urlencoded

grant_type=urn:ietf:params:oauth:grant-type:uma-ticket
&ticket=PERMISSION_TICKET_VALUE
&submit_request=false

티켓 없이 resource server의 client_id를 audience로 지정해 "내가 가진 모든 권한을 평가해 달라"고 요청할 수도 있습니다. 이는 UMA 프로토콜의 전체 왕복을 생략하는 실용적 단축 경로로 자주 쓰입니다.

POST /realms/myrealm/protocol/openid-connect/token HTTP/1.1
Host: sso.example.com
Authorization: Bearer ACCESS_TOKEN
Content-Type: application/x-www-form-urlencoded

grant_type=urn:ietf:params:oauth:grant-type:uma-ticket
&audience=document-service
&permission=document-resource#view
&response_mode=decision

response_mode=decision으로 요청하면 RPT 대신 단순한 허용 여부 JSON을 받습니다. RPT가 필요 없는 단건 결정 평가에 유용합니다.

{
  "result": true
}

발급된 RPT의 permission 클레임은 다음과 같은 구조입니다.

{
  "authorization": {
    "permissions": [
      {
        "rsid": "8f4d2e1a-resource-uuid",
        "rsname": "document-123",
        "scopes": ["view", "comment"]
      }
    ]
  },
  "aud": "document-service",
  "exp": 1781234567
}

Protection API와 리소스 동적 등록

UMA의 또 다른 축은 Protection API입니다. resource server는 service account 토큰(PAT, Protection API Token)으로 리소스를 동적으로 등록/관리할 수 있습니다. "사용자가 문서를 생성하면 그 문서를 owner와 함께 리소스로 등록"하는 패턴이 대표적입니다.

# 문서 생성 시 리소스 동적 등록
curl -X POST "https://sso.example.com/realms/myrealm/authz/protection/resource_set" \
  -H "Authorization: Bearer PAT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "document-123",
    "type": "urn:document-service:resources:document",
    "owner": "jdoe",
    "ownerManagedAccess": true,
    "resource_scopes": ["view", "edit", "delete", "share"],
    "uris": ["/api/documents/123"]
  }'

ownerManagedAccess를 켜면 리소스 소유자가 Account Console에서 직접 다른 사용자에게 접근을 공유하거나 요청을 승인/거부할 수 있습니다. "내 문서를 동료에게 공유" 같은 사용자 주도 공유 시나리오가 UMA의 본래 설계 목적입니다.

Policy Enforcer 실전 설정 (Java)

이론을 코드로 내려보겠습니다. Keycloak이 제공하는 keycloak-policy-enforcer 라이브러리는 Java 애플리케이션에 PEP를 끼워 넣는 표준 방법입니다(구 Keycloak adapter는 deprecated되었고, Spring Security + policy enforcer 조합이 현재 권장 경로입니다).

<!-- pom.xml -->
<dependency>
  <groupId>org.keycloak</groupId>
  <artifactId>keycloak-policy-enforcer</artifactId>
  <version>26.0.0</version>
</dependency>

enforcer 설정 파일입니다.

{
  "realm": "myrealm",
  "auth-server-url": "https://sso.example.com",
  "resource": "document-service",
  "credentials": {
    "secret": "CLIENT_SECRET_FROM_VAULT"
  },
  "http-method-as-scope": true,
  "lazy-load-paths": true,
  "enforcement-mode": "ENFORCING",
  "paths": [
    {
      "path": "/api/documents/*",
      "claim-information-point": {
        "claims": {
          "request.ip": "{request.remoteAddr}"
        }
      }
    },
    {
      "path": "/api/health",
      "enforcement-mode": "DISABLED"
    }
  ]
}

Spring Security 필터 체인에 enforcer를 연결합니다.

@Configuration
@EnableWebSecurity
public class SecurityConfig {

  @Bean
  public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
      .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
      .authorizeHttpRequests(authz -> authz.anyRequest().authenticated())
      .addFilterAfter(createPolicyEnforcerFilter(), BearerTokenAuthenticationFilter.class);
    return http.build();
  }

  private ServletPolicyEnforcerFilter createPolicyEnforcerFilter() {
    return new ServletPolicyEnforcerFilter(request -> {
      try (InputStream is = getClass().getResourceAsStream("/policy-enforcer.json")) {
        return JsonSerialization.readValue(is, PolicyEnforcerConfig.class);
      } catch (IOException e) {
        throw new UncheckedIOException(e);
      }
    });
  }
}

설정 포인트를 짚어보면 다음과 같습니다.

Decision Strategy — 정책 충돌의 해석

permission 하나에 여러 정책이 붙으면, 결과를 어떻게 합산할지가 decision strategy입니다.

전략의미사용 예
Unanimous (기본)모든 정책이 Permit이어야 Permit보안 우선, 기본값 권장
Affirmative하나라도 Permit이면 Permit"관리자이거나 소유자이거나"
ConsensusPermit 수가 Deny 수보다 많으면 Permit드묾, 투표형 시나리오

흔한 함정은 Affirmative가 필요한 자리에 Unanimous를 두는 것입니다. "소유자 정책 + 관리자 role 정책"을 Unanimous로 묶으면 "소유자이면서 동시에 관리자"만 통과합니다. OR 의미라면 permission의 decision strategy를 Affirmative로 바꾸거나, aggregated policy 안에서 전략을 지정해야 합니다.

또 하나, resource server 전체 수준의 decision strategy(여러 permission이 같은 리소스에 걸릴 때의 합산)도 별도로 존재합니다. permission 수준과 resource server 수준의 전략을 혼동하면 디버깅이 어려우니, Admin Console의 Evaluate 탭에서 시뮬레이션하며 확인하는 습관을 들이세요. Evaluate 탭은 특정 사용자/리소스/스코프 조합에 대해 어떤 정책이 어떤 결과를 냈는지 단계별로 보여주는, 이 기능군에서 가장 유용한 디버깅 도구입니다.

성능 고려사항

세밀한 인가는 공짜가 아닙니다. 설계 시 다음을 검토해야 합니다.

경험적 가이드라인
------------------------------------------------------
리소스 수 < 1만, 평가 QPS < 수백   → Keycloak authz 단독으로 충분
리소스 수 10만+, 관계 기반 판단    → 외부 ReBAC (OpenFGA) 병행 검토
정책이 코드/데이터 중심 (배포 파이프라인 정책 등) → OPA 병행 검토

외부 인가 엔진과의 비교 — OPA, OpenFGA

2026년의 인가 생태계에서 Keycloak Authorization Services는 유일한 선택지가 아닙니다. 대표적인 두 외부 엔진과 비교해 봅시다.

항목Keycloak AuthzOPA (Rego)OpenFGA (Zanzibar 계열)
모델resource/scope/policy범용 정책 엔진 (ABAC 강점)관계 튜플 그래프 (ReBAC)
정책 언어Admin UI + 정책 유형Rego (선언적 언어)DSL (관계 모델 정의)
데이터 위치Keycloak DB입력으로 주입 (사이드카/번들)자체 튜플 스토어
강점IdP와 통합, UMA, 사용자 주도 공유인프라 전반의 범용 정책대규모 관계 질의 (listObjects)
약점초대규모 리소스, 관계 그래프데이터 동기화는 별도 과제IdP 기능 없음, 별도 운영
표준UMA 2.0, OAuth사실상 표준 (CNCF)Zanzibar 논문 기반, OpenID AuthZEN 논의 참여

핵심 통찰은 이들이 경쟁 관계라기보다 계층이 다르다는 점입니다. Keycloak은 "누가 인증되었고 어떤 토큰을 가졌는가"의 진실의 원천이고, OPA/OpenFGA는 그 토큰을 입력 중 하나로 받아 더 복잡한 결정을 내리는 PDP가 될 수 있습니다.

함께 쓰는 패턴

실무에서 검증된 조합 패턴은 다음과 같습니다.

패턴 A: Keycloak(인증+coarse) + OPA(fine-grained, 인프라 정책)
+--------+   JWT   +-------------+  input(JWT claims,  +-----+
| Client +-------->+ API Gateway +-------------------->+ OPA |
+--------+         |  / Service  |   resource attrs)   +-----+
                   +-------------+<--------------------+
                                     allow / deny

패턴 B: Keycloak(인증) + OpenFGA(관계 기반 인가)
+--------+   JWT   +----------+  check(user, relation, +---------+
| Client +-------->+ Service  +----------------------->+ OpenFGA |
+--------+         +----------+        object)         +---------+
                        |                                   ^
                        |  쓰기 시 관계 튜플 동기화            |
                        +-----------------------------------+

Keycloak Authorization Services가 가장 빛나는 영역은 UMA 기반의 사용자 주도 리소스 공유(Account Console 통합)와 IdP와 정책 관리의 단일화가 주는 운영 단순성입니다. 반대로 수백만 객체의 관계 질의가 핵심이라면 처음부터 ReBAC 엔진을 별도 레이어로 두는 편이 낫습니다.

운영 베스트 프랙티스

트러블슈팅 노트

증상원인 후보대응
모든 요청이 403enforcement ENFORCING + 경로 미매핑경로 설정, PERMISSIVE로 격리 테스트
소유자인데 거부됨decision strategy가 UnanimousAffirmative로 변경 또는 aggregated 재구성
RPT에 permission이 비어 있음audience 누락, 리소스 미매칭audience 파라미터, 리소스 URI 패턴 확인
평가가 느림lazy-load 미사용, 리소스 과다lazy-load-paths, 타입 단위 리소스로 재설계
토큰이 너무 큼전체 권한을 RPT에 포함permission 파라미터로 범위 축소, decision 모드
정책 변경이 반영 안 됨enforcer 캐시캐시 TTL 확인, 재기동/무효화

마치며

Keycloak Authorization Services는 "권한 로직을 코드에서 꺼내 중앙에서 선언적으로 관리한다"는 목표를 OAuth/UMA 표준 위에서 구현한, 생각보다 깊이 있는 프레임워크입니다. resource/scope/policy/permission의 4분할 모델과 decision strategy를 이해하면 RBAC으로는 표현 불가능했던 요구를 우아하게 풀 수 있고, UMA 2.0의 permission ticket 흐름은 사용자 주도 공유라는 독자적 가치를 제공합니다.

동시에 한계도 분명합니다. 수백만 리소스의 관계 질의는 Zanzibar 계열 엔진의 영역이고, 인프라 전반의 범용 정책은 OPA의 영역입니다. 2026년의 현실적인 정답은 "Keycloak으로 인증과 coarse-grained를, 필요한 곳에 ReBAC/정책 엔진을 계층으로 얹는" 구성이며, 그 모든 계층의 입력이 되는 신뢰할 수 있는 토큰을 만드는 것이 Keycloak의 변치 않는 역할입니다. 다음 글에서는 Keycloak의 관측성 — 메트릭, 감사 로그, 이벤트 기반 모니터링을 다룹니다.

참고 자료

댓글

아직 댓글이 없습니다.

로그인하면 댓글을 쓸 수 있습니다