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 JobPod에만 마운트됩니다.
설정
프로덕션 모드 설정
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: 인프라 관리 도구를 위한 SSOlumie: 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)
lumie 및 lumie-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