Zum Hauptinhalt springen

Keycloak

Keycloak은 오픈소스 Identity and Access Management (IAM) 솔루션으로, OIDC/OAuth2 기반의 인증 및 인가 서비스를 제공합니다. Lumie 인프라에서는 중앙 인증 서버 역할을 하며, Google, Kakao 소셜 로그인 연동을 지원합니다.

아키텍처

구성 요소

Keycloak Server

  • 이미지: zot.lumie-infra.com/keycloak/keycloak:26.5.4
  • 모드: Production (최적화된 빌드)
  • 프록시: Kong Gateway 뒤에서 실행
  • 데이터베이스: 공유 infra-db 클러스터 사용

데이터베이스 설정

Keycloak은 공유 PostgreSQL 클러스터(infra-db)를 사용합니다:

database:
vendor: postgres
hostname: infra-db-rw.infra-db.svc.cluster.local
port: 5432
database: keycloak
username: keycloak
existingSecret: keycloak-db-bootstrap
existingSecretKey: password

Vault 시크릿 연동

데이터베이스 자격증명, 관리자 패스워드, OAuth 클라이언트 시크릿은 Vault에서 관리됩니다:

vaultStaticSecrets:
# 데이터베이스 자격증명
- name: keycloak-db-bootstrap-static
path: infrastructure/postgresql
destination:
name: keycloak-db-bootstrap
transformation:
templates:
username: keycloak
password: "{{ .Secrets.POSTGRES_PASSWORD }}"
rolloutRestartTargets:
- kind: StatefulSet
name: keycloak

# Keycloak 관리자 패스워드
- name: keycloak-secrets-static
path: infrastructure/keycloak
destination:
name: keycloak-secrets
transformation:
templates:
KEYCLOAK_ADMIN_PASSWORD: "{{ .Secrets.KEYCLOAK_ADMIN_PASSWORD }}"
rolloutRestartTargets:
- kind: StatefulSet
name: keycloak

# OAuth 시크릿 — realm-sync Job(keycloak-config-cli)이 ${KEY} 플레이스홀더를 치환하여 Admin REST API로 주입
- name: keycloak-oauth-secrets-static
path: tokens
destination:
name: keycloak-oauth-secrets
transformation:
templates:
KEYCLOAK_CODER_CLIENT_SECRET: "{{ .Secrets.KEYCLOAK_CODER_CLIENT_SECRET }}"
KEYCLOAK_VAULT_CLIENT_SECRET: "{{ .Secrets.KEYCLOAK_VAULT_CLIENT_SECRET }}"
KEYCLOAK_ZOT_CLIENT_SECRET: "{{ .Secrets.KEYCLOAK_ZOT_CLIENT_SECRET }}"
KEYCLOAK_GITEA_CLIENT_SECRET: "{{ .Secrets.KEYCLOAK_GITEA_CLIENT_SECRET }}"

keycloak-oauth-secrets는 Keycloak 서버 Pod가 아닌 realm-sync Job Pod에만 마운트됩니다.

설정

프로덕션 모드 설정

Realm 콘텐츠(클라이언트, 사용자, 소셜 로그인)는 ArgoCD PostSync 훅으로 실행되는 realm-sync Job(keycloak-config-cli)이 관리합니다. Keycloak 서버 자체는 --import-realm 플래그 없이 시작하며, Realm 임포트 ConfigMap을 마운트하지 않습니다.

# 프로덕션 모드로 시작 (Realm 임포트는 realm-sync Job이 담당)
command:
- "/opt/keycloak/bin/kc.sh"
args:
- "start"

# 프록시 설정 (Kong Gateway 뒤에서 실행)
proxy:
enabled: true
mode: xforwarded

http:
relativePath: /

# 호스트명 및 관리자 자격증명 설정
extraEnv: |
- name: KC_HOSTNAME
value: "auth.lumie-edu.com" # OIDC 흐름을 위한 외부 접근 호스트명
- name: KC_HOSTNAME_STRICT
value: "false"
- name: KEYCLOAK_ADMIN
value: admin
- name: KEYCLOAK_ADMIN_PASSWORD
valueFrom:
secretKeyRef:
name: keycloak-secrets
key: KEYCLOAK_ADMIN_PASSWORD

KC_VAULT / KC_VAULT_DIR 설정은 제거되었습니다. 이전에는 Realm JSON 내 ${vault.KEY} 플레이스홀더 확장에 사용되었지만, file vault provider는 클라이언트 시크릿 필드를 지원하지 않습니다. 현재는 realm-sync Job이 환경 변수 치환을 수행하고 Keycloak Admin REST API로 직접 설정을 적용합니다.

시작 프로브 설정

Keycloak은 첫 시작 시 최적화된 설정을 빌드하므로 충분한 시간이 필요합니다:

startupProbe: |
httpGet:
path: '/health'
port: http-internal
scheme: HTTP
initialDelaySeconds: 60
timeoutSeconds: 1
failureThreshold: 120 # 최대 10분 대기 (120 * 5초 = 600초)
periodSeconds: 5

데이터베이스 준비 상태 확인

dbchecker:
enabled: true
image:
repository: zot.lumie-infra.com/library/busybox
tag: "1.37"

추가 볼륨 마운트

Keycloak 서버 Pod에는 별도의 볼륨이 마운트되지 않습니다. Realm 임포트 ConfigMap과 OAuth 시크릿은 realm-sync Job Pod에서만 마운트됩니다.

리소스 설정

컴퓨팅 리소스

resources:
requests:
cpu: 100m
memory: 1Gi
limits:
memory: 1Gi # CPU limit 없음 (성능 최적화)

# 단일 인스턴스 실행 (Anti-affinity 비활성화)
affinity: ""

모니터링 설정

# Prometheus 메트릭 활성화
metrics:
enabled: true

# 헬스 체크 활성화
health:
enabled: true

# ServiceMonitor 생성
serviceMonitor:
enabled: true

접근 방법

Keycloak 관리 UI는 Teleport를 통해서만 접근 가능하며, OIDC 인증 흐름을 위한 auth.lumie-edu.com 도메인은 외부 인그레스를 통해 직접 접근 가능합니다.

Teleport를 통한 관리 UI 접근

apps:
- name: keycloak
uri: http://keycloak-keycloakx-http.keycloak.svc.cluster.local
labels:
env: prod
category: security
# Teleport를 통한 Keycloak 관리 UI 접근
tsh apps login keycloak
tsh apps open keycloak

# 또는 로컬 프록시
tsh proxy app keycloak --port 8080
# 브라우저에서 http://localhost:8080/admin 접근

OIDC 인증 흐름을 위한 외부 인그레스

additionalIngresses:
- name: auth
serviceName: keycloak-keycloakx-http
servicePort: 80
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
hosts:
- host: auth.lumie-edu.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: auth-keycloak-tls
hosts:
- auth.lumie-edu.com

OIDC 설정

Realm 구성

Lumie 인프라에서는 세 가지 Realm을 운영합니다:

  • infra: 인프라 관리 도구를 위한 SSO
  • lumie: Lumie 교육 플랫폼 프로덕션 환경
  • lumie-dev: Lumie 교육 플랫폼 개발 환경

infra Realm (인프라 관리용)

{
"realm": "infra",
"enabled": true,
"displayName": "Lumie Infrastructure",
"sslRequired": "external",
"registrationAllowed": false,
"resetPasswordAllowed": true,
"bruteForceProtected": true,
"failureFactor": 5,
"roles": {
"realm": [
{"name": "admin", "description": "Infrastructure administrator"},
{"name": "developer", "description": "Developer access"}
]
},
"clients": [],
"users": [
{
"username": "bluemayne",
"enabled": true,
"emailVerified": true,
"email": "bluemayne@lumie-edu.com",
"firstName": "Bluemayne",
"realmRoles": ["admin", "developer"],
"credentials": [{"type": "password", "value": "changeme", "temporary": true}]
}
]
}

lumie Realm (프로덕션 환경)

{
"realm": "lumie",
"enabled": true,
"displayName": "Lumie Education",
"sslRequired": "external",
"registrationAllowed": false,
"resetPasswordAllowed": true,
"bruteForceProtected": true,
"failureFactor": 5,
"accountTheme": "keycloak.v2",
"loginTheme": "keycloak.v2",
"identityProviders": [
{
"alias": "google",
"providerId": "google",
"enabled": true,
"trustEmail": true,
"storeToken": false,
"linkOnly": false,
"firstBrokerLoginFlowAlias": "first broker login",
"config": {
"clientId": "${vault.GOOGLE_CLIENT_ID}",
"clientSecret": "${vault.GOOGLE_CLIENT_SECRET}",
"defaultScope": "openid email profile",
"syncMode": "IMPORT"
}
},
{
"alias": "kakao",
"displayName": "Kakao",
"providerId": "oidc",
"enabled": true,
"trustEmail": false,
"storeToken": false,
"linkOnly": false,
"firstBrokerLoginFlowAlias": "first broker login",
"config": {
"clientId": "${vault.KAKAO_CLIENT_ID}",
"clientSecret": "${vault.KAKAO_CLIENT_SECRET}",
"authorizationUrl": "https://kauth.kakao.com/oauth/authorize",
"tokenUrl": "https://kauth.kakao.com/oauth/token",
"userInfoUrl": "https://kapi.kakao.com/v2/user/me",
"defaultScope": "profile_nickname account_email",
"syncMode": "IMPORT",
"clientAuthMethod": "client_secret_post"
}
}
],
"clients": [
{
"clientId": "lumie-app",
"name": "Lumie Application",
"enabled": true,
"protocol": "openid-connect",
"publicClient": false,
"secret": "${vault.KEYCLOAK_LUMIE_CLIENT_SECRET}",
"standardFlowEnabled": true,
"directAccessGrantsEnabled": false,
"redirectUris": [
"https://lumie-edu.com/api/auth/v1/oauth2/keycloak/callback"
],
"webOrigins": [
"https://lumie-edu.com"
],
"defaultClientScopes": ["openid", "profile", "email"]
}
]
}

lumie-dev Realm (개발 환경)

{
"realm": "lumie-dev",
"enabled": true,
"displayName": "Lumie Education (Dev)",
"sslRequired": "external",
"registrationAllowed": false,
"resetPasswordAllowed": true,
"bruteForceProtected": true,
"failureFactor": 5,
"accountTheme": "keycloak.v2",
"loginTheme": "keycloak.v2",
"identityProviders": [
{
"alias": "google",
"providerId": "google",
"enabled": true,
"trustEmail": true,
"storeToken": false,
"linkOnly": false,
"firstBrokerLoginFlowAlias": "first broker login",
"config": {
"clientId": "${vault.GOOGLE_CLIENT_ID}",
"clientSecret": "${vault.GOOGLE_CLIENT_SECRET}",
"defaultScope": "openid email profile",
"syncMode": "IMPORT"
}
},
{
"alias": "kakao",
"displayName": "Kakao",
"providerId": "oidc",
"enabled": true,
"trustEmail": false,
"storeToken": false,
"linkOnly": false,
"firstBrokerLoginFlowAlias": "first broker login",
"config": {
"clientId": "${vault.KAKAO_CLIENT_ID}",
"clientSecret": "${vault.KAKAO_CLIENT_SECRET}",
"authorizationUrl": "https://kauth.kakao.com/oauth/authorize",
"tokenUrl": "https://kauth.kakao.com/oauth/token",
"userInfoUrl": "https://kapi.kakao.com/v2/user/me",
"defaultScope": "profile_nickname account_email",
"syncMode": "IMPORT",
"clientAuthMethod": "client_secret_post"
}
}
],
"clients": [
{
"clientId": "lumie-app",
"name": "Lumie Application (Dev)",
"enabled": true,
"protocol": "openid-connect",
"publicClient": false,
"secret": "${vault.KEYCLOAK_LUMIE_CLIENT_SECRET}",
"standardFlowEnabled": true,
"directAccessGrantsEnabled": false,
"redirectUris": [
"https://dev.lumie-edu.com/api/auth/v1/oauth2/keycloak/callback"
],
"webOrigins": [
"https://dev.lumie-edu.com"
],
"defaultClientScopes": ["openid", "profile", "email"]
}
]
}

클라이언트 설정

Lumie 애플리케이션 클라이언트 (lumie-app)

lumielumie-dev Realm에서 사용되는 클라이언트입니다.

{
"clientId": "lumie-app",
"name": "Lumie Application",
"enabled": true,
"protocol": "openid-connect",
"publicClient": false,
"secret": "${vault.KEYCLOAK_LUMIE_CLIENT_SECRET}",
"standardFlowEnabled": true,
"directAccessGrantsEnabled": false,
"redirectUris": [
"https://lumie-edu.com/api/auth/v1/oauth2/keycloak/callback"
],
"webOrigins": [
"https://lumie-edu.com"
],
"defaultClientScopes": ["openid", "profile", "email"]
}

사용자 속성 매핑

{
"mappers": [
{
"name": "tenant-id",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-attribute-mapper",
"config": {
"user.attribute": "tenant_id",
"claim.name": "tenant_id",
"jsonType.label": "String"
}
},
{
"name": "roles",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-realm-role-mapper",
"config": {
"claim.name": "roles",
"jsonType.label": "String",
"multivalued": "true"
}
}
]
}

소셜 로그인 연동

Keycloak은 Google 및 Kakao 소셜 로그인을 지원하며, 클라이언트 ID와 시크릿은 Vault에서 가져옵니다.

Google 로그인

{
"alias": "google",
"providerId": "google",
"enabled": true,
"trustEmail": true,
"storeToken": false,
"linkOnly": false,
"firstBrokerLoginFlowAlias": "first broker login",
"config": {
"clientId": "${vault.GOOGLE_CLIENT_ID}",
"clientSecret": "${vault.GOOGLE_CLIENT_SECRET}",
"defaultScope": "openid email profile",
"syncMode": "IMPORT"
}
}

Kakao 로그인

{
"alias": "kakao",
"displayName": "Kakao",
"providerId": "oidc",
"enabled": true,
"trustEmail": false,
"storeToken": false,
"linkOnly": false,
"firstBrokerLoginFlowAlias": "first broker login",
"config": {
"clientId": "${vault.KAKAO_CLIENT_ID}",
"clientSecret": "${vault.KAKAO_CLIENT_SECRET}",
"authorizationUrl": "https://kauth.kakao.com/oauth/authorize",
"tokenUrl": "https://kauth.kakao.com/oauth/token",
"userInfoUrl": "https://kapi.kakao.com/v2/user/me",
"defaultScope": "profile_nickname account_email",
"syncMode": "IMPORT",
"clientAuthMethod": "client_secret_post"
}
}

JWT 토큰 구조

Access Token 예시

{
"exp": 1640995200,
"iat": 1640991600,
"jti": "uuid",
"iss": "https://auth.lumie-edu.com/realms/lumie",
"aud": "lumie-app",
"sub": "user-uuid",
"typ": "Bearer",
"azp": "lumie-app",
"session_state": "session-uuid",
"scope": "openid profile email",
"email_verified": true,
"name": "홍길동",
"preferred_username": "hong@lumie.kr",
"given_name": "길동",
"family_name": "홍",
"email": "hong@lumie.kr",
"tenant_id": "tenant-uuid",
"roles": ["student", "premium"]
}

보안 설정

세션 관리

{
"ssoSessionIdleTimeout": 1800,
"ssoSessionMaxLifespan": 36000,
"accessTokenLifespan": 300,
"refreshTokenMaxReuse": 0
}

패스워드 정책

{
"passwordPolicy": "length(8) and digits(1) and lowerCase(1) and upperCase(1) and specialChars(1) and notUsername"
}

브루트 포스 보호

{
"bruteForceProtected": true,
"failureFactor": 5,
"waitIncrementSeconds": 60,
"quickLoginCheckMilliSeconds": 1000,
"minimumQuickLoginWaitSeconds": 60,
"maxFailureWaitSeconds": 900,
"maxDeltaTimeSeconds": 43200
}

모니터링

Prometheus 메트릭

Keycloak은 다음 메트릭을 제공합니다:

# 활성 세션 수
keycloak_sessions_active_total

# 로그인 시도 수
keycloak_logins_total

# 토큰 발급 수
keycloak_tokens_issued_total

# 응답 시간
keycloak_request_duration_seconds

로그 모니터링

# Keycloak 로그 확인
kubectl logs -n keycloak -l app.kubernetes.io/name=keycloakx

# 로그인 실패 모니터링
kubectl logs -n keycloak -l app.kubernetes.io/name=keycloakx | grep "LOGIN_ERROR"

백업 및 복구

데이터베이스 백업

Keycloak 데이터는 PostgreSQL에 저장되므로 데이터베이스 백업으로 복구 가능합니다:

# 데이터베이스 백업 (CNPG 자동 백업 사용)
kubectl get backup -n infra-db

# 수동 백업
kubectl exec -n infra-db infra-db-1 -- pg_dump -U keycloak keycloak > keycloak-backup.sql

설정 내보내기

# Realm 설정 내보내기
kubectl exec -n keycloak deployment/keycloak -- \
/opt/keycloak/bin/kc.sh export --realm lumie --file /tmp/lumie-realm.json

# 설정 파일 복사
kubectl cp keycloak/keycloak-pod:/tmp/lumie-realm.json ./lumie-realm.json

문제 해결

시작 실패

# 시작 로그 확인
kubectl logs -n keycloak -l app.kubernetes.io/name=keycloakx

# 데이터베이스 연결 확인
kubectl exec -n keycloak deployment/keycloak -- \
pg_isready -h infra-db-rw.infra-db.svc.cluster.local -p 5432

성능 문제

# 메모리 사용량 확인
kubectl top pods -n keycloak

# JVM 힙 덤프 생성 (필요시)
kubectl exec -n keycloak deployment/keycloak -- \
jcmd 1 GC.run_finalization

인증 문제

# 토큰 검증 테스트
curl -X POST "http://localhost:8080/realms/lumie/protocol/openid-connect/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=password&client_id=lumie-app&username=test&password=test"

배포 설정

ArgoCD Application

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: keycloak
namespace: argocd
spec:
project: default
sources:
- repoURL: https://codecentric.github.io/helm-charts
chart: keycloakx
targetRevision: 7.1.7
helm:
valueFiles:
- $values/security/keycloak/helm-values.yaml
- repoURL: https://github.com/Lumie-Edu/lumie-infra.git
targetRevision: main
ref: values
path: charts/common
helm:
valueFiles:
- $values/security/keycloak/common-values.yaml
destination:
server: https://kubernetes.default.svc
namespace: keycloak
syncPolicy:
automated:
prune: true
selfHeal: true

관련 문서