티스토리 뷰
작성일: 2025년 12월 6일
카테고리: Authentication, Authorization, Architecture, Backend
키워드: Ory Kratos, Ory Hydra, Ory Keto, Ory Oathkeeper, Zero Trust, OAuth2, ReBAC
요약
ImpRun v3 플랫폼은 Ory 스택 전체(Kratos, Hydra, Keto, Oathkeeper)를 통합하여 엔터프라이즈급 인증/인가 시스템을 구축했다. 각 컴포넌트가 명확한 역할을 담당하며, Zero Trust 아키텍처를 기반으로 웹 애플리케이션과 외부 API 클라이언트를 모두 지원한다. 이 글에서는 4개 Ory 컴포넌트의 조합 방식과 실제 구현 사례를 다룬다.
Ory 스택 구성
컴포넌트별 역할
| 컴포넌트 | 역할 | 포트 |
|---|---|---|
| Ory Kratos | Identity Management - 사용자 등록, 로그인, 세션 관리 | 4433 (Public), 4434 (Admin) |
| Ory Hydra | OAuth2/OIDC Server - 토큰 발급, 클라이언트 관리 | 4444 (Public), 4445 (Admin) |
| Ory Keto | Permissions - ReBAC 기반 권한 관리 | 4466 (Read), 4467 (Write) |
| Ory Oathkeeper | Identity & Access Proxy - 인증/인가 게이트키퍼 | 4455 (Proxy), 4456 (API) |
전체 아키텍처
flowchart TB
subgraph clients["Clients"]
Browser["Browser<br/>(Next.js)"]
External["External API<br/>Client"]
M2M["M2M Service"]
end
subgraph gateway["Gateway Layer"]
Envoy["Envoy Gateway"]
Oathkeeper["Oathkeeper<br/>(IAP)"]
end
subgraph auth["Ory Auth Stack"]
Kratos["Kratos<br/>(Identity)"]
Hydra["Hydra<br/>(OAuth2)"]
Keto["Keto<br/>(Permissions)"]
end
subgraph backend["Backend"]
GoAPI["Go API Server"]
DB["PostgreSQL"]
end
Browser -->|"Session Cookie"| Envoy
External -->|"Bearer Token"| Envoy
M2M -->|"Client Credentials"| Hydra
Envoy <-->|"ext_authz"| Oathkeeper
Oathkeeper -->|"whoami"| Kratos
Oathkeeper -->|"introspect"| Hydra
Envoy -->|"X-User-* headers"| GoAPI
GoAPI -->|"Permission Check"| Keto
GoAPI --> DB
Kratos --> DB
Hydra --> DB
Keto --> DB
style gateway stroke:#dc2626,stroke-width:2px
style auth stroke:#16a34a,stroke-width:2px
style backend stroke:#2563eb,stroke-width:2px
인증 방식별 플로우
클라이언트 유형별 인증
| 클라이언트 | 인증 방식 | 토큰 타입 | Oathkeeper Handler |
|---|---|---|---|
| Web App (Next.js) | Kratos Session | Cookie | cookie_session |
| External API | Hydra OAuth2 | Bearer Token | bearer_token |
| M2M | Client Credentials | Bearer Token | bearer_token |
1. 웹 로그인 플로우 (Kratos Session)
sequenceDiagram
participant Browser
participant NextJS as Next.js
participant Kratos
participant Envoy
participant Oathkeeper
participant GoAPI as Go API
Note over Browser,GoAPI: 로그인 Flow
Browser->>NextJS: 1. /login 접속
NextJS->>Kratos: 2. init flow
Kratos-->>NextJS: flow data
NextJS-->>Browser: render form
Browser->>NextJS: 3. submit credentials
NextJS->>Kratos: submit to Kratos
Kratos-->>Browser: 4. 세션 쿠키 발급<br/>(ory_kratos_session)
Note over Browser,GoAPI: API 요청 Flow
Browser->>Envoy: 5. API 요청 (with cookie)
Envoy->>Oathkeeper: 6. ext_authz
Oathkeeper->>Kratos: 7. /sessions/whoami
Kratos-->>Oathkeeper: session info
Oathkeeper-->>Envoy: 8. OK + X-User-* headers
Envoy->>GoAPI: 9. Forward request
GoAPI-->>Browser: response
2. 외부 API 플로우 (OAuth2 Bearer Token)
sequenceDiagram
participant Client as API Client
participant Hydra
participant Envoy
participant Oathkeeper
participant GoAPI as Go API
Note over Client,GoAPI: 토큰 발급
Client->>Hydra: 1. POST /oauth2/token<br/>(client_credentials)
Hydra-->>Client: access_token
Note over Client,GoAPI: API 요청
Client->>Envoy: 2. API 요청<br/>(Authorization: Bearer ...)
Envoy->>Oathkeeper: 3. ext_authz
Oathkeeper->>Hydra: 4. POST /oauth2/introspect
Hydra-->>Oathkeeper: token info (active, scope, sub)
Oathkeeper-->>Envoy: 5. OK + headers
Envoy->>GoAPI: 6. Forward request
GoAPI-->>Client: response
3. 권한 검사 플로우 (Keto ReBAC)
sequenceDiagram
participant GoAPI as Go API
participant Keto
participant DB
Note over GoAPI,DB: Gateway 접근 권한 검사
GoAPI->>Keto: 1. Check permission<br/>(Gateway:gw-id#view@User:user-id)
Note over Keto: Relation Tuple 검색<br/>+ SubjectSet 확장
Keto->>DB: 2. Query relation tuples
DB-->>Keto: tuples
alt 직접 권한
Keto-->>GoAPI: 3a. allowed: true
else 조직 상속 권한
Note over Keto: Organization:org-id#members<br/>→ Gateway:gw-id#viewers
Keto-->>GoAPI: 3b. allowed: true
else 권한 없음
Keto-->>GoAPI: 3c. allowed: false
end
Ory 컴포넌트 상세 설정
Kratos 설정 (Identity Management)
# kratos.yml
version: v1.1.0
dsn: postgres://kratos:secret@postgres:5432/kratos
serve:
public:
base_url: http://localhost:3000/.ory/
cors:
enabled: true
allowed_origins:
- http://localhost:3000
selfservice:
default_browser_return_url: http://localhost:3000/
methods:
password:
enabled: true
oidc:
enabled: true
config:
providers:
- id: google
provider: google
client_id: ${GOOGLE_CLIENT_ID}
client_secret: ${GOOGLE_CLIENT_SECRET}
scope: [email, profile]
flows:
login:
ui_url: http://localhost:3000/auth/login
registration:
ui_url: http://localhost:3000/auth/registration
after:
password:
hooks:
- hook: session # 회원가입 후 자동 로그인
session:
lifespan: 24h
cookie:
name: ory_kratos_session
same_site: Lax
identity:
default_schema_id: user
schemas:
- id: user
url: file:///etc/config/kratos/schemas/user.schema.json
Hydra 설정 (OAuth2/OIDC)
# hydra.yml
version: v2.2.0
dsn: postgres://hydra:secret@postgres:5432/hydra
serve:
public:
cors:
enabled: true
allowed_origins:
- http://localhost:3000
urls:
self:
issuer: http://localhost:4444/
consent: http://localhost:3000/auth/consent
login: http://localhost:3000/auth/login
logout: http://localhost:3000/auth/logout
strategies:
access_token: jwt
oauth2:
allowed_top_level_claims:
- namespace
- roles
- scopes
Keto 설정 (Permissions)
# keto.yml
version: v0.14.0
dsn: postgres://keto:secret@postgres:5432/keto
namespaces:
location: file:///etc/config/keto/namespaces.keto.ts
serve:
read:
host: 0.0.0.0
port: 4466
write:
host: 0.0.0.0
port: 4467
OPL Namespace 정의:
// namespaces.keto.ts
class User implements Namespace {}
class Organization implements Namespace {
related: {
owners: User[]
admins: User[]
members: User[]
}
permits = {
manage: (ctx: Context): boolean =>
this.related.owners.includes(ctx.subject) ||
this.related.admins.includes(ctx.subject),
view: (ctx: Context): boolean =>
this.related.owners.includes(ctx.subject) ||
this.related.admins.includes(ctx.subject) ||
this.related.members.includes(ctx.subject),
}
}
class Gateway implements Namespace {
related: {
owners: (User | SubjectSet<Organization, "admins">)[]
admins: (User | SubjectSet<Organization, "admins">)[]
members: User[]
viewers: (User | SubjectSet<Organization, "members">)[]
organization: Organization[]
}
permits = {
manage: (ctx: Context): boolean =>
this.related.owners.includes(ctx.subject) ||
this.related.admins.includes(ctx.subject),
view: (ctx: Context): boolean =>
this.related.owners.includes(ctx.subject) ||
this.related.admins.includes(ctx.subject) ||
this.related.members.includes(ctx.subject) ||
this.related.viewers.includes(ctx.subject),
delete: (ctx: Context): boolean =>
this.related.owners.includes(ctx.subject),
}
}
Oathkeeper 설정 (IAP)
# oathkeeper.yml
serve:
proxy:
port: 4455
api:
port: 4456
access_rules:
repositories:
- file:///etc/config/oathkeeper/access-rules.yml
authenticators:
noop:
enabled: true
cookie_session:
enabled: true
config:
check_session_url: http://kratos:4433/sessions/whoami
preserve_path: true
extra_from: "@this"
subject_from: "identity.id"
only:
- ory_kratos_session
bearer_token:
enabled: true
config:
check_session_url: http://hydra:4445/oauth2/introspect
token_from:
header: Authorization
authorizers:
allow:
enabled: true
mutators:
header:
enabled: true
config:
headers:
X-User-Id: "{{ print .Subject }}"
X-User-Email: "{{ print .Extra.identity.traits.email }}"
errors:
fallback:
- json
handlers:
json:
enabled: true
redirect:
enabled: true
config:
to: http://localhost:3000/auth/login
Go 백엔드 통합
클라이언트 구조
// internal/infrastructure/client/ory_clients.go
package client
type OryClients struct {
Kratos *KratosClient
Hydra *HydraClient
Keto *KetoClient
}
func NewOryClients(cfg *config.OryConfig) *OryClients {
return &OryClients{
Kratos: NewKratosClient(cfg.Kratos),
Hydra: NewHydraClient(cfg.Hydra),
Keto: NewKetoClient(cfg.Keto),
}
}
통합 인증 미들웨어
Oathkeeper가 전달한 헤더를 사용하거나, 직접 Kratos/Hydra를 호출하는 미들웨어:
// internal/interface/middleware/auth.go
func AuthMiddleware(oryClients *client.OryClients, useOathkeeper bool) gin.HandlerFunc {
return func(c *gin.Context) {
// 1. Oathkeeper 모드: 헤더에서 사용자 정보 추출
if useOathkeeper {
userID := c.GetHeader("X-User-Id")
if userID != "" {
user := &entity.UserSession{
ID: uuid.MustParse(userID),
Email: c.GetHeader("X-User-Email"),
TokenType: "oathkeeper",
}
c.Set("user", user)
c.Next()
return
}
}
// 2. Bearer Token 검증 (Hydra)
authHeader := c.GetHeader("Authorization")
if strings.HasPrefix(authHeader, "Bearer ") {
token := strings.TrimPrefix(authHeader, "Bearer ")
introspection, err := oryClients.Hydra.Introspect(c.Request.Context(), token)
if err == nil && introspection.Active {
user := &entity.UserSession{
ID: uuid.MustParse(introspection.Subject),
TokenType: "oauth2",
Scopes: introspection.Scope,
}
c.Set("user", user)
c.Next()
return
}
}
// 3. Session Cookie 검증 (Kratos)
sessionCookie, err := c.Cookie("ory_kratos_session")
if err == nil && sessionCookie != "" {
session, err := oryClients.Kratos.WhoAmI(c.Request.Context(), sessionCookie)
if err == nil && session.Active {
user := &entity.UserSession{
ID: uuid.MustParse(session.Identity.ID),
Email: session.Identity.Traits.Email,
TokenType: "session",
}
c.Set("user", user)
c.Next()
return
}
}
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"error": "No valid authentication provided",
})
}
}
Keto 권한 검사 미들웨어
// internal/interface/middleware/permission.go
func GatewayPermissionMiddleware(ketoClient *client.KetoClient, permission string) gin.HandlerFunc {
return func(c *gin.Context) {
gatewayID := c.Param("gw")
if gatewayID == "" {
c.Next()
return
}
user, exists := c.Get("user")
if !exists {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"error": "Unauthorized",
})
return
}
userSession := user.(*entity.UserSession)
// Keto 권한 검사
allowed, err := ketoClient.Check(c.Request.Context(), KetoCheckRequest{
Namespace: "Gateway",
Object: gatewayID,
Relation: permission,
SubjectID: userSession.ID.String(),
})
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
"error": "Permission check failed",
})
return
}
if !allowed {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{
"error": "Forbidden",
"resource": "Gateway",
"action": permission,
})
return
}
c.Set("currentGateway", gatewayID)
c.Next()
}
}
라우터 설정
// internal/interface/router/router.go
func SetupRouter(oryClients *client.OryClients) *gin.Engine {
r := gin.Default()
// Public endpoints
public := r.Group("/api/public")
{
public.GET("/health", healthCheck)
public.GET("/directory/*path", getDirectory)
}
// Protected endpoints
protected := r.Group("/api")
protected.Use(AuthMiddleware(oryClients, true))
{
// User endpoints
protected.GET("/me", getMe)
protected.GET("/organizations", listOrganizations)
// Gateway endpoints with permission checks
gw := protected.Group("/gateways/:gw")
{
gw.GET("", GatewayPermissionMiddleware(oryClients.Keto, "view"), getGateway)
gw.PUT("", GatewayPermissionMiddleware(oryClients.Keto, "manage"), updateGateway)
gw.DELETE("", GatewayPermissionMiddleware(oryClients.Keto, "delete"), deleteGateway)
// Products
gw.GET("/products", GatewayPermissionMiddleware(oryClients.Keto, "view"), listProducts)
gw.POST("/products", GatewayPermissionMiddleware(oryClients.Keto, "manage"), createProduct)
// Config
gw.PUT("/config", GatewayPermissionMiddleware(oryClients.Keto, "publish_config"), publishConfig)
}
}
return r
}
Next.js 프론트엔드 통합
Ory Client 설정
// shared/api/ory.ts
import { Configuration, FrontendApi } from '@ory/client';
const KRATOS_URL = '/.ory';
export const kratosClient = new FrontendApi(
new Configuration({
basePath: KRATOS_URL,
baseOptions: {
withCredentials: true,
},
})
);
Next.js Proxy 설정
// next.config.ts
const nextConfig: NextConfig = {
async rewrites() {
return [
{
source: '/.ory/:path*',
destination: `${process.env.KRATOS_URL || 'http://localhost:4433'}/:path*`,
},
];
},
};
Auth Provider
// entities/auth/model/auth-provider.tsx
'use client';
import { createContext, useContext, useEffect, useState } from 'react';
import { Session } from '@ory/client';
import { kratosClient } from '@/shared/api/ory';
interface AuthContextType {
session: Session | null;
isAuthenticated: boolean;
isLoading: boolean;
logout: () => Promise<void>;
}
const AuthContext = createContext<AuthContextType | null>(null);
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [session, setSession] = useState<Session | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
kratosClient.toSession()
.then(({ data }) => {
setSession(data);
})
.catch(() => {
setSession(null);
})
.finally(() => {
setIsLoading(false);
});
}, []);
const logout = async () => {
const { data } = await kratosClient.createBrowserLogoutFlow();
window.location.href = data.logout_url;
};
return (
<AuthContext.Provider value={{
session,
isAuthenticated: !!session,
isLoading,
logout,
}}>
{children}
</AuthContext.Provider>
);
}
export const useAuth = () => {
const context = useContext(AuthContext);
if (!context) throw new Error('useAuth must be used within AuthProvider');
return context;
};
Docker Compose 전체 구성
services:
postgres:
image: postgres:16-alpine
environment:
POSTGRES_USER: root
POSTGRES_PASSWORD: secret
volumes:
- ./docker/postgres/init:/docker-entrypoint-initdb.d
- postgres_data:/var/lib/postgresql/data
kratos:
image: oryd/kratos:v1.1.0
environment:
DSN: postgres://kratos:secret@postgres:5432/kratos
ports:
- "4433:4433"
- "4434:4434"
volumes:
- ./docker/ory/kratos:/etc/config/kratos
command: serve -c /etc/config/kratos/kratos.yml --dev --watch-courier
depends_on:
- postgres
hydra:
image: oryd/hydra:v2.2.0
environment:
DSN: postgres://hydra:secret@postgres:5432/hydra
URLS_CONSENT: http://localhost:3000/auth/consent
URLS_LOGIN: http://localhost:3000/auth/login
ports:
- "4444:4444"
- "4445:4445"
volumes:
- ./docker/ory/hydra:/etc/config/hydra
command: serve all -c /etc/config/hydra/hydra.yml --dev
depends_on:
- postgres
keto:
image: oryd/keto:v0.14.0
environment:
DSN: postgres://keto:secret@postgres:5432/keto
ports:
- "4466:4466"
- "4467:4467"
volumes:
- ./docker/ory/keto:/etc/config/keto
command: serve -c /etc/config/keto/keto.yml
depends_on:
- postgres
oathkeeper:
image: oryd/oathkeeper:v0.40.7
ports:
- "4455:4455"
- "4456:4456"
volumes:
- ./docker/ory/oathkeeper:/etc/config/oathkeeper
command: serve --config /etc/config/oathkeeper/oathkeeper.yml
depends_on:
- kratos
- hydra
envoy:
image: envoyproxy/envoy:v1.32-latest
ports:
- "8080:8080"
- "9901:9901"
volumes:
- ./docker/envoy/envoy.yaml:/etc/envoy/envoy.yaml
depends_on:
- oathkeeper
api:
build:
context: ./apps/api
dockerfile: Dockerfile
environment:
- KRATOS_PUBLIC_URL=http://kratos:4433
- KRATOS_ADMIN_URL=http://kratos:4434
- HYDRA_PUBLIC_URL=http://hydra:4444
- HYDRA_ADMIN_URL=http://hydra:4445
- KETO_READ_URL=http://keto:4466
- KETO_WRITE_URL=http://keto:4467
- USE_OATHKEEPER=true
depends_on:
- kratos
- hydra
- keto
web:
build:
context: ./apps/web
dockerfile: Dockerfile
ports:
- "3000:3000"
environment:
- KRATOS_URL=http://kratos:4433
depends_on:
- api
- kratos
mailhog:
image: mailhog/mailhog
ports:
- "8025:8025"
volumes:
postgres_data:
데이터 흐름 요약
인증 데이터 흐름
1. 사용자 로그인 (Kratos)
Browser → Next.js → Kratos → Session Cookie
2. API 요청
Browser (Cookie) → Envoy → Oathkeeper → Kratos (whoami)
→ Headers 추가
→ Go API
3. 권한 검사
Go API → Keto (check) → allowed/denied
4. 사용자 데이터 동기화
Go API (첫 요청) → Kratos Identity → User 테이블 생성컴포넌트별 책임
| 컴포넌트 | 핵심 질문 | 담당 기능 |
|---|---|---|
| Kratos | WHO - 사용자가 누구인가? | 회원가입, 로그인, 세션 관리, 소셜 로그인 (OIDC), 이메일 인증, 비밀번호 복구 |
| Hydra | TOKEN - 접근 토큰은 무엇인가? | Access Token / Refresh Token 발급, Client Credentials (M2M), Token Introspection |
| Keto | CAN - 무엇을 할 수 있는가? | ReBAC 기반 권한 모델, Relation Tuple 관리, 권한 상속 (Organization → Gateway) |
| Oathkeeper | GATE - 통과할 수 있는가? | 모든 요청 검증, Kratos/Hydra 연동, 사용자 정보 헤더 주입 |
운영 고려사항
성능
- 각 Ory 컴포넌트는 stateless → 수평 확장 가능
- PostgreSQL 연결 풀 적절히 설정
- Oathkeeper 세션 캐싱으로 Kratos/Hydra 부하 감소
보안
- Admin API 격리: 4434, 4445, 4467 포트는 내부 네트워크에서만 접근
- HTTPS 필수: 프로덕션에서 모든 통신 암호화
- Secret 관리: 환경 변수 또는 Vault 사용
모니터링
각 컴포넌트는 Prometheus 메트릭 제공:
# 메트릭 엔드포인트
Kratos: http://kratos:4434/metrics
Hydra: http://hydra:4445/metrics
Keto: http://keto:4468/metrics
Oathkeeper: http://oathkeeper:9000/metrics
참고 자료
공식 문서
관련 문서
- Ory Kratos를 활용한 사용자 인증 시스템 구축 - Identity Management 상세 구현
- Ory Hydra OAuth2/OIDC 구현 가이드 - OAuth2 토큰 발급 및 클라이언트 관리
- Ory Keto를 활용한 ReBAC 기반 권한 관리 시스템 구축 - 권한 모델 및 Relation Tuple
- Ory Oathkeeper를 활용한 Zero Trust IAP 구현 가이드 - Zero Trust Proxy 설정
'실제 경험과 인사이트를 AI와 함께 정리한 글' 카테고리의 다른 글
| CLAUDE.md 공식 가이드 정리: 프로젝트 컨텍스트 자동화 (0) | 2025.12.11 |
|---|---|
| Claude Code 사용 리포트: 27일간 12억 토큰의 기록 (0) | 2025.12.11 |
| Ory Oathkeeper를 활용한 Zero Trust IAP 구현 가이드 (0) | 2025.12.07 |
| Ory Keto를 활용한 ReBAC 기반 권한 관리 시스템 구축 (0) | 2025.12.07 |
| Ory Kratos를 활용한 사용자 인증 시스템 구축: ImpRun 프로젝트 적용 사례 (0) | 2025.12.07 |
공지사항
최근에 올라온 글
최근에 달린 댓글
- Total
- Today
- Yesterday
링크
TAG
- 개발 도구
- api gateway
- claude code
- PYTHON
- Tailwind CSS
- knowledge graph
- LangChain
- Go
- authentication
- Developer Tools
- react
- AI agent
- architecture
- ai 개발 도구
- Tax Analysis
- authorization
- AI Development
- Ontology
- troubleshooting
- Claude
- backend
- workflow
- AI
- frontend
- LLM
- SHACL
- Rag
- Kubernetes
- security
- Next.js
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| 8 | 9 | 10 | 11 | 12 | 13 | 14 |
| 15 | 16 | 17 | 18 | 19 | 20 | 21 |
| 22 | 23 | 24 | 25 | 26 | 27 | 28 |
글 보관함