-
ImpRun 인증/인가 아키텍처: Ory 스택 통합 구현 가이드실제 경험과 인사이트를 AI와 함께 정리한 글 2025. 12. 7. 10:58
작성일: 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_sessionExternal API Hydra OAuth2 Bearer Token bearer_tokenM2M Client Credentials Bearer Token bearer_token1. 웹 로그인 플로우 (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: response2. 외부 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: response3. 권한 검사 플로우 (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 endOry 컴포넌트 상세 설정
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.jsonHydra 설정 (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 - scopesKeto 설정 (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: 4467OPL 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/loginGo 백엔드 통합
클라이언트 구조
// 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