AI 지원 AuthZ 리뷰: Ory Kratos에서의 읽기 권한 경계(Permission Boundaries) 분석
요약
AI를 활용하여 Ory Kratos의 권한 부여(AuthZ) 로직을 리뷰하는 방법론을 소개합니다. AI가 생성한 수많은 보안 가설을 인간이 검증하고 제거하는 '제거 표(Kill table)' 중심의 접근 방식을 강조합니다.
핵심 포인트
- AI의 역할은 버그 발견이 아닌 저렴한 보안 가설의 과잉 생성임
- 리뷰의 핵심 결과물은 발견 사항이 아닌 기각된 가설의 기록(Kill table)임
- Ory Kratos를 대상으로 권한 경계 설계와 AI 오탐 방지 사례 연구 수행
- AI 지원 리뷰는 오탐을 줄이고 인간의 검증 효율을 높이는 데 집중해야 함
AI를 사용하여 권한 부여(Authorization)를 리뷰하는 시리즈의 두 번째 글입니다 — 보고서를 남발하기 위함이 아닙니다.
참조 문서: AuthZ Smell Catalog.
1. AuthZ 리뷰가 취약점 남발이 아닌 이유
보안 분야에서 AI가 할 수 있는 가장 저렴한 일은 의구심을 생성하는 것입니다. 코드베이스(Codebase)에 모델을 적용하면, 커피를 다 마시기도 전에 50개의 "가능한 IDOR(Insecure Direct Object Reference)"를 던져줄 것입니다. 그중 거의 대부분은 틀렸습니다 — 세 줄 위에서 방어되고 있거나, 데이터 계층(Data layer)에서 범위가 지정되었거나, 모델이 보지 못한 경계(Boundary)에서 보호되고 있기 때문입니다.
이러한 범람은 바로 여러 버그 바운티(Bug bounty) 프로그램들이 2026년에 규정을 강화하거나 중단했던 정확한 이유입니다. 그들은 확신에 차 있고 그럴듯하지만 틀린 보고서들에 잠겨 있었습니다.
따라서 이 리뷰는 일반적인 루프를 뒤집습니다. AI의 역할은 버그를 찾는 것이 아니라, 가설을 저렴하게 과잉 생성하는 것입니다. 저의 역할은 그 가설들을 **제거(Kill)**하는 것입니다. 그 제거 과정을 견뎌내고 살아남은 것만이 인간의 시간을 들일 가치가 있는 유일한 것이며, 무엇이 죽었는지에 대한 기록은 무엇이 살았는지에 대한 기록보다 더 유용합니다.
그러므로 정직한 리뷰의 결과물은 발견 사항(Finding)이 아니라, **제거 표(Kill table)**입니다.
2. 대상 및 범위
- 대상 (Target): Ory Kratos — 오픈 소스 ID(Identity) 및 사용자 관리 서버 (로그인, 등록, 복구, 인증, 세션, 셀프 서비스 설정). 소스 공개(Source-available), Apache-2.0 라이선스.
- Kratos를 선택한 이유: 인가(Authorization) 오류가 발생하기 딱 좋은 형태를 갖추고 있기 때문입니다. 즉, 다중 ID(Multiple identities), 공개 API(Public API)와 관리자 API(Admin API)의 공존, 그리고 (Ory의 호스팅 제품의 경우) 멀티 테넌시(Multi-tenancy) 환경을 모두 포함합니다. 경계(Boundary)가 취약하다면 바로 이런 곳에서 드러나게 됩니다.
- 본 글의 범위: 공개 저장소(Public repository)의 단일 테넌트 오픈 소스(OSS) 빌드에 대한 소스 코드 읽기(Source reading)만 수행했습니다. 호스팅된 대상은 전혀 건드리지 않았습니다. 여기서 다루는 내용은 미공개 발견 사항(Undisclosed finding)이 아닙니다. 핵심은 **방법론과 경계 설계(Boundary design)**이며, 관련이 있는 경우 설계가 제가 테스트한 가설들에 어떻게 대응했는지를 보여주는 것입니다. 이는 저희가 추적하는 재현 티어(Reproduction tiers)에 대응됩니다. 아래의 모든 내용은
repo_only이며, 저는 이것이 실제 라이브 제품에 도달한다고 암시하는 대신 명시적으로 밝힙니다.
본 리뷰의 주장 범위 및 한계. 이 제한적인 저장소 전용(Repo-only) 리뷰에서, 제가 테스트한 가설들은 기각(Killed)되었습니다. 이것은 Kratos에 취약점이 없다는 주장이 아니며, 보안 감사(Security audit)도 아닙니다. 이는 AI 지원 인가(AuthZ) 리뷰가 어떻게 오탐(False positives)을 피할 수 있는지, 즉 의심 사항을 그대로 배포하는 대신 어떻게 _기각(Kill)_할 수 있는지에 대한 사례 연구입니다.
3. AI가 의심한 사항
저는 저렴한 탐지 도구들이 AuthZ Smell Catalog를 바탕으로 과도하게 결과물을 생성하도록 두었습니다. 필터링되지 않은 가공되지 않은 후보 목록은 다음과 같습니다:
- H1 — Admin API에는 코드 내 인가(Authorization)가 없음. Admin API의 Identity CRUD, 세션 삭제, courier 엔드포인트는 핸들러 내에서 요청별 권한 확인(per-request permission check)을 수행하지 않습니다. (Catalog §01 object-id substitution, §13 middleware-only authorization.)
- H2 — 교차 ID / 교차 테넌트(Cross-identity / cross-tenant) 읽기.
/sessions/whoami, ID 조회, 또는 관리자 ID 호출을 통해 한 ID가 다른 ID의 데이터를 읽을 수 있는가? (Catalog §04 tenant id from client input, §05 cross-tenant leak via related object.) - H3 — 복구 / 인증 토큰 재사용. 일회용 토큰이 실제로 일회용인가? (Catalog §09 invite/share token reuse.)
- H4 — 설정 흐름(Settings-flow)에서의 ID 혼동. 셀프 서비스 설정 흐름이 다른 ID의 속성(traits)을 가리키도록 설정할 수 있는가? (Catalog §02 ownership vs reachability, §07 stale privilege.)
- H5 — 페이로드(Payload)를 통한 테넌트 할당. 관리자 생성/업데이트 시 임의의 네트워크 ID를 설정하여 테넌트 경계를 넘을 수 있는가? (Catalog §04.)
다섯 가지 확신 있는 가설입니다. 이 부분은 AI가 잘하는 영역입니다. 이제 AI가 할 수 없는 부분입니다.
4. 각 가설을 검증(Kill)하려 시도한 방법
규칙: 구체적인 테스트가 그렇지 않다고 말하기 전까지는 각 가설이 설계 의도(by-design)라고 가정하며, 기본적으로는 해당 가설을 기각(killing)하는 방향으로 진행합니다. 소스 코드만 검토하는 경우, "테스트"란 단순히 보호되지 않아 보이는 핸들러를 찾는 것이 아니라, 불변성(invariant)이 실제로 깨지는 사용자 도달 가능한(user-reachable) 경로를 추적할 수 있는지를 의미합니다.
H1 — Admin API의 인가(Authorization) "누락" → 기각됨 (설계 의도).
Kratos는 의도적으로 Admin API에 내장된 인가 기능을 포함하지 않고 출시합니다. Ory의 자체 문서에 따르면, Admin API는 네트워크 경계(ingress, 리버스 프록시, Oathkeeper 등)에서 보호되어야 하며 절대 공개적으로 노출되어서는 안 됩니다. 따라서 "핸들러 내에 인가 확인이 없음"은 보호 장치가 누락된 것이 아니라, 보호 장치가 한 단계 바깥 레이어에 존재하는 것이며, 이는 Catalog **§13 (middleware/deployment-layer authorization)**에서 언급된 전형적인 오탐(false-positive) 형태입니다. "Admin API가 인증 없이 Identity CRUD를 허용한다"는 보고는 설계 의도에 따른 것이므로 그대로 종결됩니다. 기각됨.
H2 — 교차 ID / 교차 테넌트 읽기(Cross-identity / cross-tenant read) → 기각됨 (chokepoint design).
이 부분은 매우 흥미롭습니다. Kratos는 핸들러(handler) 전반에 테넌트 체크를 흩뿌려 놓지 않습니다. 대신 영속성 계층(persistence layer)이 모든 쿼리를 **네트워크 컨텍스츄얼라이저(network Contextualizer)**를 통해 실행하며, 이 과정에서 SQL에 네트워크 ID(nid)를 주입합니다. 즉, 데이터 액세스 계층(data-access layer) 자체가 중앙에서 테넌트별로 필터링을 수행합니다. 핸들러는 경계를 넘어 실수로 데이터를 읽을 수 없습니다. 왜냐하면 경계가 핸들러 하단, 즉 모든 읽기 작업이 통과하는 단 한 곳에서 강제되기 때문입니다. 퍼블릭 API(public API)에서 ID 액세스는 클라이언트가 제공한 ID가 아니라, _세션(session)_의 ID로부터 파생됩니다. H2를 무너뜨리려면 영속성 계층(persister)을 완전히 우회하는 읽기 경로를 찾아야 하는데, 이 빌드에서는 사용자가 도달 가능한 경로를 찾지 못했습니다. 기각됨. 또한 하나의 패턴으로서 주목할 만한 점은, 데이터 액세스 계층에 테넌트 필터를 집중시킴으로써 해당 클래스 전체를 단일 감사 가능 지점(auditable point)으로 수렴시킨다는 것입니다. 이것이 바로 이 특정 가설들이 여기서 종결된 이유입니다 (Catalog §B).
H3 — 토큰 재사용(Token reuse) → 기각됨.
복구 및 검증 토큰은 단회성(single-use)이며 유효 기간이 정해져 있습니다. 사용(redemption) 시 동일한 트랜잭션 내에서 해당 토큰은 무효화됩니다. 사용 후 재전송(replay)은 실패합니다. 기각됨.
H4 — 설정 흐름 ID 혼동(Settings-flow identity confusion) → 기각됨.
설정 흐름(settings flow)은 인증된 세션으로부터 확인된 ID에 바인딩됩니다. 수정되는 ID는 클라이언트 입력값에서 가져오지 않으므로, 해당 흐름을 다른 사용자의 속성(traits)으로 타겟팅할 수 없습니다. 기각됨 (Catalog §02 — 읽기 도달 가능성(read-reachability)이 쓰기 도달 가능성(write-reachability)을 의미하지 않으며, 여기서는 읽기조차 세션에 바인딩되어 있음).
H5 — 페이로드로부터의 테넌트(Tenant from payload) → 기각됨.
네트워크 ID는 요청 본문(request body)이 아닌 컨텍스트(context)로부터 파생됩니다. 관리자의 생성/업데이트(create/update) 시 외부의 nid를 몰래 주입(smuggle)할 수 없습니다. 기각됨.
5. 킬 테이블 (The kill table)
전체 리뷰의 결과물을 한 화면에 정리하면 다음과 같습니다:
| # | 가설 (Hypothesis) | 카탈로그 (Catalog) | 판결 (Verdict) | 기각 사유 (Why it died) |
|---|---|---|---|---|
| H1 | Admin API 권한 부여 (authz) 누락 | §01, §13 | 설계 의도 (by-design) | 권한 부여 (authz)가 핸들러가 아닌 네트워크 경계(network boundary)에 위치함 — 문서화되어 있음 |
| ... |
다섯 개의 가설을 검토했습니다. 발견된 결함은 0개입니다. 이것은 실패한 리뷰가 아니라 성공적인 리뷰입니다 — 정확히 말하자면, Kratos가 완벽하다는 보증서가 아니라, 다섯 개의 가설에 대한 성공적인 리뷰입니다.
6. 명명할 가치가 있는 오탐 (False-positive) 패턴
여기서 나타난 두 가지 형태는 Kratos를 훨씬 넘어 일반화될 수 있습니다:
- 배포 계층(Deployment-layer)의 권한 부여를 권한 부여 "누락"으로 오인하는 경우. 코드 내부에 체크 로직이 없는 엔드포인트는 코드 외부에서 이를 보호하는 장치가 없을 때만 위험 신호(red flag)가 됩니다. 관리 평면(Admin planes), 내부 서비스, 그리고 "인그레스(ingress)에서 이를 보호한다"는 설계 방식은 모두 단순한 AI를 혼란에 빠뜨립니다. 보고하기 전에 다른 곳에 보호 장치가 존재할 수 있는지 항상 질문하십시오 (카탈로그 §13).
- 병목 지점(Chokepoint) 경계가 핸들러별로 보호되지 않는 것처럼 보이는 경우. 실제 경계가 단일 데이터 액세스 계층(data-access layer)에서 강제될 때, 개별 핸들러는 모두 보호 장치가 없는 것처럼 보입니다. 올바른 리뷰 질문은 "이 핸들러가 체크를 하는가?"에서 "병목 지점을 통과하지 않고 데이터에 도달할 수 있는 방법이 있는가?"로 전환되어야 합니다 (카탈로그 §B, §12 대체 경로).
7. 살아남은 것과 살아남지 못한 것
내가 테스트한 가설 중 소스 코드만으로 진행한 리뷰에서 살아남은 것은 하나도 없었습니다. 그리고 그 _이유_는 공유할 가치가 있습니다: Kratos는 테넌트 경계(tenant boundary)를 한 곳(persister의 Contextualizer)에 집중시키며, 클라이언트 입력이 아닌 세션(session)으로부터 신원(identity)을 도출합니다. 이러한 설계 선택이 바로 나의 다섯 가지 가설 중 네 가지를 하나의 질문으로 수렴하게 만든 결정적인 요인이었으며, 그 질문에는 명확한 답이 있었습니다.
만약 리뷰를 계속 진행한다면, 유일하게 정직한 다음 단계는 persister를 거치지 않고 영속화된 데이터에 도달할 수 있는 모든 인그레스(ingress)를 나열하는 것입니다 — 백그라운드 작업(background jobs), 임포트(imports), 모든 로우 쿼리(raw query) 등이 해당됩니다. OSS 빌드에는 사용자가 도달할 수 있는 경로는 없습니다. 이러한 부정적 결과(negative result)는 실제 유의미한 신호이며, 이는 repo_only 등급에 해당합니다: 즉, 특정 호스팅 배포 환경에 대해서도 이 결과가 유효하다고 주장하는 것은 아닙니다.
8. AI 지원 보안 리뷰를 위한 교훈
- AI가 과하게 생성하도록 두되, 인간을 킬 스위치(kill switch)로 만드세요. 가치는 의심이 아니라 거절(rejection)에 있습니다.
- 설계에 의한 것(by-design)을 기본값으로 설정하세요. 구체적이고 도달 가능한 경로를 통해 그렇지 않음이 증명될 때까지는 해당 경계가 의도된 것이라고 가정하십시오. 이 단 하나의 편향(bias)만 있었어도 2026년의 대부분의 보고 스팸(report-spam)을 방지할 수 있었을 것입니다.
- 병목 지점(chokepoint)을 먼저 찾으세요. 코드베이스가 중앙에서 경계를 강제한다면, 리뷰는 "병목 지점이 우회되는 경우가 있는가?"라는 질문으로 축소됩니다. 이는 훨씬 더 작고, 훨씬 더 정직한 질문입니다.
- 재현 등급(reproduction tier)을 명확히 밝히세요.
repo_only는hosted_confirmed가 아닙니다. 자신이 어떤 등급을 가지고 있는지 말하십시오. 이 둘을 혼동하는 것이 오픈 소스(OSS) 읽기가 허위 버그 바운티(bounty) 주장으로 변질되는 방식입니다. - 승리뿐만 아니라 킬 테이블(kill table)을 공개하세요. 거절(rejections) 자체가 바로 포트폴리오입니다.
9. 이것이 AuthZ 냄새 카탈로그(AuthZ Smell Catalog)에 기여하는 방식
각각의 킬(kill)은 카탈로그 항목의 confirm/kill 열을 정교하게 만들었습니다. 이 열은 실제 버그와 설계에 의한 동작(by-design behavior)을 구분합니다:
- §13은 배포 계층 권한 부여(deployment-layer-authz) 오탐(false positive) 항목을 얻었습니다 (H1로부터).
- §B / §04는 병목 지점-컨텍스추얼라이저(chokepoint-Contextualizer) 확인 테스트 항목을 얻었습니다: 응답 필드를 추적하고, 어떤 읽기 경로가 중앙 스코퍼(central scoper)를 건너뛰는지 질문하십시오 (H2로부터).
- §09는 일회용 토큰 재사용(single-use-token replay) 테스트 항목을 얻었습니다 (H3로부터).
카탈로그는 정적인 목록이 아닙니다. 모든 실제 결과 — 심지어 깔끔한 설계에 의한 결과(by-design result)라 할지라도 — 더 날카로운 킬 테스트를 카탈로그에 다시 공급합니다. 그 피드백 루프가 자산이지, 항목의 개수가 아닙니다.
10. 다음 단계: OSS 사례 연구에서 실제 버그 바운티 타겟으로
이 리뷰는 결과 장부(outcome ledger)에 정확히 한 줄을 생성합니다. 방어된 타겟이라는, 정직한 종류의 한 줄입니다:
date=2026-07-04, program=Ory Kratos (self-directed OSS review), source_type=oss_source_available,
class=tenant_boundary, repro_tier=repo_only, human_verdict=by_design, final_status=not_applicable,
payout_usd=0, lesson="Contextualizer/nid chokepoint concentrates the tenant boundary; admin-API
...
원장(ledger)의 1번 행은 지급(payout)이 되어서는 안 됩니다. 그것은 _진실(true)_이어야 합니다. 여기서 다음 단계는 새로 추가된 권한 경계(Permission Boundary)(새로운 RBAC, 워크스페이스, 빌링, 또는 SSO/SCIM 기능)를 가진 단일 소스 공개(source-available) 타겟 — 즉, 아직 검증되지 않은 표면(surface) — 을 선정하여, 동일한 '과잉 생성 후 제거(over-generate-then-kill)' 루프를 통과시키고 원장의 2번 행으로 기록하는 것입니다. 타겟은 하나여야 합니다. 열 개가 아니라 말입니다.
저는 AI를 사용하여 후보들을 거절하고, 살아남은 소수의 후보를 검증하기 위해 인간을 사용합니다. 만약 이 접근 방식이 유용하다면, AuthZ Smell Catalog는 이 시리즈가 구축해 나가는 동반 참조 자료입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기