Skip to content
Published on

차세대 디지털 아이덴티티와 SSO, 그리고 Keycloak 실무 완벽 가이드

Authors
  • Name
    Twitter

1. 패스워드 피로와 SSO의 등장

현대 비즈니스 환경에서 사용하는 어플리케이션이 기하급수적으로 늘어나면서 **패스워드 피로도(Password Fatigue)**는 심각한 보안 위협이자 생산성 저하의 원인이 되었다. 이를 해결하기 위해 등장한 SSO(Single Sign-On) 기술은 단 한 번의 인증으로 신뢰 관계가 구축된 모든 서비스에 접근할 수 있게 해준다.

SSO를 통해 얻을 수 있는 핵심 가치는 다음과 같다.

  • MFA 중앙화: 다요소 인증을 한 곳에서 관리하여 일관된 보안 정책 적용
  • 즉각적 권한 회수: 퇴사자나 역할 변경 시 IdP에서 한 번에 접근 차단
  • 제로 트러스트 초석: "Never Trust, Always Verify" 원칙의 기반

2. SSO 구현 아키텍처

SSO 시스템은 환경에 따라 세 가지 모델로 나뉜다.

모델방식적합 환경
에이전트 기반개별 서버에 에이전트 설치레거시 환경
에이전트리스API Gateway / Reverse Proxy로 트래픽 제어클라우드 네이티브
아이덴티티 페더레이션도메인 간 아이덴티티 공유B2B 협업, 멀티 조직

3. 기술 표준 비교: SAML vs OAuth 2.0 vs OIDC

항목SAML 2.0OAuth 2.0OIDC
목적인증(Authentication)인가(Authorization)인증 + 인가
데이터 형식XML (Assertion)JSONJSON (JWT)
토큰SAML AssertionAccess TokenID Token + Access Token
주요 사용처엔터프라이즈 웹앱API 권한 위임모바일, SPA, MSA
복잡도높음중간낮음

**OIDC(OpenID Connect)**는 OAuth 2.0 위에 아이덴티티 계층을 얹어 JWT 형태로 가볍게 인증을 처리하며, 현대 마이크로서비스 환경의 사실상 표준이다.


4. Keycloak 핵심 아키텍처

Keycloak은 SAML, OAuth 2.0, OIDC 표준을 모두 지원하는 오픈소스 IAM 솔루션이다. Red Hat이 지원하며 CNCF 인큐베이팅 프로젝트다.

4.1 핵심 계층 구조

Keycloak Instance
  └── Realm (논리적 격리 단위, 테넌트)
        ├── Clients (인증을 요청하는 어플리케이션)
        ├── Users (사용자)
        ├── Groups (사용자 그룹)
        ├── Roles (Realm Roles + Client Roles)
        ├── Identity Providers (외부 IdP 연동)
        ├── Authentication Flows (인증 흐름 정의)
        └── Authorization Services (세밀한 인가 정책)
  • Realm: 독립된 테넌트 공간. master Realm은 관리용, 실제 서비스는 별도 Realm 생성
  • Client: OAuth/OIDC 클라이언트 어플리케이션 (Confidential / Public / Bearer-only)
  • Roles & Groups: 권한 관리의 핵심. Realm Role은 전체 범위, Client Role은 특정 클라이언트 범위

4.2 기술 스택

컴포넌트역할
Quarkus고성능 Java 프레임워크 기반 런타임
Infinispan인메모리 분산 캐시 (세션, 토큰 동기화)
PostgreSQL (등)사용자, 정책 등 영구 데이터 저장

5. 프로덕션 배포

5.1 Docker 배포

개발 모드 (빠른 시작, H2 내장 DB):

docker run --name keycloak -p 8080:8080 \
  -e KC_BOOTSTRAP_ADMIN_USERNAME=admin \
  -e KC_BOOTSTRAP_ADMIN_PASSWORD=change_me \
  quay.io/keycloak/keycloak:latest \
  start-dev

프로덕션 모드 (TLS + PostgreSQL 필수):

docker run --name keycloak -p 8443:8443 -p 9000:9000 -m 2g \
  -e KC_BOOTSTRAP_ADMIN_USERNAME=admin \
  -e KC_BOOTSTRAP_ADMIN_PASSWORD=change_me \
  quay.io/keycloak/keycloak:latest start \
  --hostname=https://auth.example.com \
  --https-certificate-file=/opt/keycloak/conf/cert.pem \
  --https-certificate-key-file=/opt/keycloak/conf/key.pem \
  --db=postgres \
  --db-url=jdbc:postgresql://db-host:5432/keycloak \
  --db-username=keycloak \
  --db-password=change_me \
  --health-enabled=true \
  --metrics-enabled=true

5.2 Kubernetes Operator 배포

# CRD 및 Operator 설치
kubectl apply -f https://raw.githubusercontent.com/keycloak/keycloak-k8s-resources/26.5.4/kubernetes/keycloaks.k8s.keycloak.org-v1.yml
kubectl apply -f https://raw.githubusercontent.com/keycloak/keycloak-k8s-resources/26.5.4/kubernetes/keycloakrealmimports.k8s.keycloak.org-v1.yml
kubectl create namespace keycloak
kubectl -n keycloak apply -f https://raw.githubusercontent.com/keycloak/keycloak-k8s-resources/26.5.4/kubernetes/kubernetes.yml

Keycloak CR 매니페스트:

apiVersion: k8s.keycloak.org/v2alpha1
kind: Keycloak
metadata:
  name: production-kc
  namespace: keycloak
spec:
  instances: 3
  db:
    vendor: postgres
    host: postgres-db
    usernameSecret:
      name: keycloak-db-secret
      key: username
    passwordSecret:
      name: keycloak-db-secret
      key: password
  http:
    tlsSecret: keycloak-tls-secret
  hostname:
    hostname: auth.example.com
  proxy:
    headers: xforwarded

5.3 keycloak.conf 프로덕션 설정

# Hostname
hostname=https://auth.example.com
hostname-admin=https://admin.auth.example.com

# Database
db=postgres
db-url=jdbc:postgresql://db:5432/keycloak
db-username=keycloak
db-password=${vault.db-password}
db-pool-max-size=100

# TLS
http-enabled=false
https-port=8443
https-certificate-file=/etc/certs/tls.crt
https-certificate-key-file=/etc/certs/tls.key

# Proxy
proxy-headers=xforwarded

# Cache / Clustering
cache=ispn

# Health & Metrics (port 9000)
health-enabled=true
metrics-enabled=true

# Logging
log=console,file
log-console-output=json

6. 인증(Authentication) 플로우

6.1 OIDC 엔드포인트

모든 엔드포인트는 Well-Known URL에서 자동 탐색 가능하다.

GET /realms/{realm}/.well-known/openid-configuration
엔드포인트경로
Authorization/realms/{realm}/protocol/openid-connect/auth
Token/realms/{realm}/protocol/openid-connect/token
Userinfo/realms/{realm}/protocol/openid-connect/userinfo
Logout/realms/{realm}/protocol/openid-connect/logout
JWKS/realms/{realm}/protocol/openid-connect/certs
Introspect/realms/{realm}/protocol/openid-connect/token/introspect

6.2 Authorization Code Flow + PKCE

웹 애플리케이션과 SPA/모바일에 권장되는 표준 플로우다.

1단계 - 인가 요청 (브라우저 리다이렉트):

GET /realms/myrealm/protocol/openid-connect/auth?
  response_type=code&
  client_id=my-app&
  redirect_uri=https://myapp.example.com/callback&
  scope=openid profile email&
  state=random-state-value&
  code_challenge=BASE64URL(SHA256(code_verifier))&
  code_challenge_method=S256

2단계 - 토큰 교환 (서버 사이드):

curl -X POST \
  https://auth.example.com/realms/myrealm/protocol/openid-connect/token \
  -d "grant_type=authorization_code" \
  -d "code=AUTHORIZATION_CODE" \
  -d "redirect_uri=https://myapp.example.com/callback" \
  -d "client_id=my-app" \
  -d "client_secret=my-secret" \
  -d "code_verifier=ORIGINAL_CODE_VERIFIER"

PKCE는 코드 가로채기 공격을 방지한다. code_verifier는 클라이언트가 생성한 랜덤 문자열이고, code_challenge는 그 SHA256 해시다. 토큰 교환 시 원본 code_verifier를 제출하여 검증한다.

6.3 Client Credentials Flow

서비스 간(M2M) 인증에 사용한다.

curl -X POST \
  https://auth.example.com/realms/myrealm/protocol/openid-connect/token \
  -d "grant_type=client_credentials" \
  -d "client_id=my-service" \
  -d "client_secret=my-service-secret" \
  -d "scope=openid"

Confidential 클라이언트에는 내장 Service Account가 있다. Clients > my-service > Service Account Roles에서 역할을 할당한다.

6.4 Client 타입 선택 가이드

타입시크릿사용처
Confidential있음서버 사이드 웹앱, 백엔드 서비스
Public없음SPA, 모바일 앱 (PKCE 필수)
Bearer-only검증만REST API 서비스 (Resource Server)

7. 인가(Authorization) 체계

7.1 RBAC (Role-Based Access Control)

역할 기반 접근 제어는 사용자에게 역할을 할당하고, 역할에 따라 접근을 허용/거부한다.

  • Realm Role: 전체 Realm 범위 (예: admin, user)
  • Client Role: 특정 클라이언트 범위 (예: my-app:editor)
  • Composite Role: 여러 역할을 묶은 상위 역할

7.2 ABAC (Attribute-Based Access Control)

속성 기반 접근 제어는 사용자의 IP, 접속 시간, 부서 정보 등을 종합하여 동적으로 판단한다.

Keycloak Authorization Services는 다양한 정책 타입을 지원한다.

정책 타입설명
Role-basedRealm/Client 역할 평가
Group-based그룹 멤버십 평가
Time-based시간대/기간 제한
Client-based요청 클라이언트 제한
JavaScript커스텀 JS 조건
Regex패턴 매칭
Aggregated여러 정책 조합

7.3 권한 모델

"X(사용자)가 Y(액션)를 Z(리소스)에 수행할 수 있는가?"

# RPT(Requesting Party Token) 요청으로 권한 확인
curl -X POST \
  https://auth.example.com/realms/myrealm/protocol/openid-connect/token \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -d "grant_type=urn:ietf:params:oauth:grant-type:uma-ticket" \
  -d "audience=my-resource-server"

결정 전략:

  • Affirmative: 하나의 정책이라도 허용하면 접근 허용
  • Unanimous: 모든 정책이 허용해야 접근 허용

8. Identity Brokering과 Social Login

Keycloak은 외부 IdP와의 연동을 기본 지원한다.

8.1 지원 IdP

유형지원 목록
Social LoginGoogle, GitHub, Facebook, Microsoft, LinkedIn, GitLab 등
프로토콜 기반OIDC v1.0, OAuth 2.0, SAML 2.0

8.2 Google IdP 설정 예시

  1. Google Cloud Console에서 OAuth 2.0 자격증명 생성
  2. Redirect URI: https://auth.example.com/realms/{realm}/broker/google/endpoint
  3. Keycloak Admin Console > Identity Providers > Google 추가
  4. Client ID와 Client Secret 입력

8.3 LDAP/Active Directory 연동

User Federation을 통해 LDAP과 연동할 수 있다.

설정Active Directory일반 LDAP
Connection URLldaps://ad-server:636ldap://ldap-server:389
Users DNOU=Users,DC=corp,DC=example,DC=comou=People,dc=example,dc=com
Username AttributesAMAccountNameuid
UUID AttributeobjectGUIDentryUUID

동기화 모드:

  • READ_ONLY: LDAP에서 읽기만 (가장 안전)
  • WRITABLE: Keycloak에서 LDAP 속성 수정 가능
  • UNSYNCED: 변경사항을 Keycloak 로컬에만 저장

9. 어플리케이션 연동 실무

9.1 Spring Boot Resource Server

application.yml:

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: https://auth.example.com/realms/myrealm
          jwk-set-uri: https://auth.example.com/realms/myrealm/protocol/openid-connect/certs

SecurityConfig.java:

@Configuration
@EnableMethodSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/public/**").permitAll()
                .requestMatchers("/api/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(jwt -> jwt
                    .jwtAuthenticationConverter(jwtAuthenticationConverter())
                )
            );
        return http.build();
    }

    private JwtAuthenticationConverter jwtAuthenticationConverter() {
        JwtGrantedAuthoritiesConverter converter = new JwtGrantedAuthoritiesConverter();
        converter.setAuthoritiesClaimName("realm_access.roles");
        converter.setAuthorityPrefix("ROLE_");
        JwtAuthenticationConverter jwtConverter = new JwtAuthenticationConverter();
        jwtConverter.setJwtGrantedAuthoritiesConverter(converter);
        return jwtConverter;
    }
}

9.2 Next.js (Auth.js / NextAuth.js)

.env.local:

AUTH_KEYCLOAK_ID=my-nextjs-app
AUTH_KEYCLOAK_SECRET=my-client-secret
AUTH_KEYCLOAK_ISSUER=https://auth.example.com/realms/myrealm

auth.ts (Auth.js v5):

import NextAuth from 'next-auth'
import Keycloak from 'next-auth/providers/keycloak'

export const { handlers, auth, signIn, signOut } = NextAuth({
  providers: [Keycloak],
})

Keycloak Client 설정에서 Redirect URI를 등록한다:

https://myapp.example.com/api/auth/callback/keycloak

9.3 React SPA (keycloak-js)

import Keycloak from 'keycloak-js'

const keycloak = new Keycloak({
  url: 'https://auth.example.com',
  realm: 'myrealm',
  clientId: 'my-spa',
})

// Authorization Code Flow + PKCE로 초기화
const authenticated = await keycloak.init({
  onLoad: 'login-required',
  pkceMethod: 'S256',
  checkLoginIframe: false,
})

// API 호출 시 토큰 사용
const response = await fetch('/api/data', {
  headers: {
    Authorization: `Bearer ${keycloak.token}`,
  },
})

// 토큰 만료 전 자동 갱신
keycloak.onTokenExpired = () => {
  keycloak.updateToken(30).catch(() => keycloak.login())
}

// 역할 확인
keycloak.hasRealmRole('admin')
keycloak.hasResourceRole('editor', 'my-resource-server')

9.4 Node.js Express

const express = require('express')
const session = require('express-session')
const Keycloak = require('keycloak-connect')

const app = express()
const memoryStore = new session.MemoryStore()

app.use(
  session({
    secret: 'my-secret',
    resave: false,
    saveUninitialized: true,
    store: memoryStore,
  })
)

const keycloak = new Keycloak(
  { store: memoryStore },
  {
    realm: 'myrealm',
    'auth-server-url': 'https://auth.example.com/',
    resource: 'my-node-app',
  }
)

app.use(keycloak.middleware())

// 인증 필수 엔드포인트
app.get('/api/protected', keycloak.protect(), (req, res) => {
  res.json({ message: 'Authenticated!' })
})

// 특정 역할 필수
app.get('/api/admin', keycloak.protect('realm:admin'), (req, res) => {
  res.json({ message: 'Admin access granted' })
})

app.listen(3000)

10. MSA 환경의 Token Relay 패턴

마이크로서비스 아키텍처에서 API Gateway가 Keycloak과 연동하여 JWT 토큰을 획득한 후, 내부 서비스로 요청을 전달하는 패턴이다.

[Client][API Gateway][Keycloak] (토큰 획득/검증)
         Authorization: Bearer <JWT>
    [Service A][Service B][Service C]

각 서비스가 IdP와 직접 통신할 필요 없이 JWT의 서명만 로컬에서 검증하므로 뛰어난 성능과 보안성을 확보할 수 있다. Keycloak의 JWKS 엔드포인트(/protocol/openid-connect/certs)에서 공개키를 가져와 캐싱하면 된다.


11. Admin REST API 활용

11.1 토큰 획득

# 패스워드 방식
ACCESS_TOKEN=$(curl -s -X POST \
  "https://auth.example.com/realms/master/protocol/openid-connect/token" \
  -d "grant_type=password" \
  -d "client_id=admin-cli" \
  -d "username=admin" \
  -d "password=admin-password" | jq -r '.access_token')

# Service Account 방식 (자동화 권장)
ACCESS_TOKEN=$(curl -s -X POST \
  "https://auth.example.com/realms/master/protocol/openid-connect/token" \
  -d "grant_type=client_credentials" \
  -d "client_id=my-admin-client" \
  -d "client_secret=my-admin-secret" | jq -r '.access_token')

11.2 사용자 관리

# 사용자 생성
curl -X POST \
  "https://auth.example.com/admin/realms/myrealm/users" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "username": "newuser",
    "email": "newuser@example.com",
    "enabled": true,
    "credentials": [{
      "type": "password",
      "value": "temp-password",
      "temporary": true
    }]
  }'

# 사용자 검색
curl -X GET \
  "https://auth.example.com/admin/realms/myrealm/users?username=johndoe" \
  -H "Authorization: Bearer $ACCESS_TOKEN"

# 역할 할당
curl -X POST \
  "https://auth.example.com/admin/realms/myrealm/users/{user-id}/role-mappings/realm" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '[{"id": "ROLE_ID", "name": "admin"}]'

12. 고가용성(HA) 아키텍처

12.1 분산 아키텍처

                    ┌─────────────┐
Load Balancer│
                    └──────┬──────┘
              ┌────────────┼────────────┐
              ▼            ▼            ▼
        ┌──────────┐ ┌──────────┐ ┌──────────┐
        │Keycloak 1│ │Keycloak 2│ │Keycloak 3        └────┬─────┘ └────┬─────┘ └────┬─────┘
Infinispan    (세션 동기화)             └──────────────┴───────────────┘
                  ┌──────┴──────┐
PostgreSQL                    (공유 DB)                  └─────────────┘
  • 정적 데이터 (사용자, 정책): 공유 DB (PostgreSQL)에 저장
  • 동적 데이터 (세션, 토큰): Infinispan 분산 캐시로 노드 간 동기화

12.2 Kubernetes Health Probe 설정

spec:
  containers:
    - name: keycloak
      livenessProbe:
        httpGet:
          path: /health/live
          port: 9000
        initialDelaySeconds: 30
        periodSeconds: 10
      readinessProbe:
        httpGet:
          path: /health/ready
          port: 9000
        initialDelaySeconds: 30
        periodSeconds: 10
      startupProbe:
        httpGet:
          path: /health/started
          port: 9000
        failureThreshold: 30
        periodSeconds: 5

12.3 Cross-DC 복제 (외부 Infinispan)

지리적으로 분리된 데이터센터 간 동기화가 필요한 경우:

cache=ispn
cache-remote-host=infinispan.example.com
cache-remote-port=11222
cache-remote-username=keycloak
cache-remote-password=change_me
cache-remote-tls-enabled=true

13. 모니터링 및 관측성

13.1 Prometheus 메트릭

# prometheus.yml
scrape_configs:
  - job_name: 'keycloak'
    metrics_path: '/metrics'
    scheme: https
    static_configs:
      - targets: ['keycloak-host:9000']

이벤트 메트릭 활성화:

bin/kc.sh start \
  --metrics-enabled=true \
  --event-metrics-user-enabled=true \
  --event-metrics-user-tags=realm,client,idp \
  --event-metrics-user-events=LOGIN,LOGIN_ERROR,LOGOUT,TOKEN_REFRESH

13.2 Grafana 대시보드

Keycloak 공식 Grafana 대시보드를 제공한다.

대시보드용도
keycloak-troubleshooting-dashboard.jsonSLI 및 진단 분석
keycloak-capacity-planning-dashboard.json부하 추정, 로그인 플로우 분석

다운로드: github.com/keycloak/keycloak-grafana-dashboard


14. 마무리

성공적인 SSO 구축을 위한 핵심 요약이다.

  1. 프로토콜: OIDC를 기본으로 선정하고, 레거시 엔터프라이즈 앱에만 SAML 적용
  2. 인가 체계: RBAC으로 기본 구조를 잡고, 세밀한 제어가 필요한 경우 ABAC 결합
  3. 고가용성: 최소 3개 노드 + Infinispan 분산 캐시 + 공유 DB 구성
  4. 어플리케이션 연동: Spring Boot는 Resource Server JWT 검증, SPA는 keycloak-js + PKCE, Next.js는 Auth.js 활용
  5. 모니터링: Prometheus 메트릭 + Grafana 대시보드로 로그인 실패율, 토큰 발급량 등 추적

Keycloak은 이 모든 것을 오픈소스로 제공하는 차세대 디지털 아이덴티티 거버넌스의 핵심이다.


References