본문으로 건너뛰기

CLI SSO 운영 (Keycloak gend-cli client)

GenD CLI 의 사람용 SSO 흐름 — gend auth login — 은 Keycloak realm 의 gend-cli public client 에 의존한다. 본 문서는 운영자가 본 client 를 환경별로 (Kind dev / AKS prod) 동기화하고 검증하는 방법을 설명한다.

사용자용 빠른 시작: gend-cli 빠른 시작

머신용 자격증명: M2M 서비스 계정 인증

1. Client 의 책무

gend-cliRFC 8252 (Authorization Code + Loopback + PKCE)RFC 8628 (Device Authorization Grant) 둘 다 지원하는 public client 다.

속성이유
publicClienttrueCLI 는 client_secret 안전 보관 불가 (RFC 8252)
standardFlowEnabledtrueLoopback Authorization Code
directAccessGrantsEnabledfalsepassword grant 금지 (보안)
serviceAccountsEnabledfalseM2M 은 별도 gend-service / /admin/service-clients
pkce.code.challenge.methodS256public client 필수 보안
oauth2.device.authorization.grant.enabledtrueDevice Flow
oauth2.device.polling.interval5폴링 빈도 (초)
redirectUrishttp://127.0.0.1:*, http://localhost:*, urn:ietf:wg:oauth:2.0:oobRFC 8252 권장
protocolMappersgend-api audience + groupsgend-ui 와 동일 권한 모델

정의는 infra/keycloak/base/realm-export.json 에 단일 진실 공급원으로 존재한다. 환경별 redirectUris 차이는 overlay (infra/keycloak/overlays/<env>/) 에서 patch.

2. 동기화 절차

GenD Keycloak realm 은 ArgoCD ApplicationSet 미포함 — 수동 명령으로만 prod 에 반영된다.

Kind (dev / staging)

make keycloak-realm-sync KCTX=kind-gend-local

realm-sync-job Pod 가 realm-export.json 을 entity 별로 split 한 뒤 kcadm 으로 idempotent CRUD 한다. 신규 gend-cli client 는 첫 실행 시 CREATE, 이후 변경은 UPDATE.

AKS prod (정비창 필수)

# 사전 점검
kubectl --context aks-genos-prod -n gend get pods -l app=keycloak
kubectl --context aks-genos-prod -n gend get configmap keycloak-realm-export -o yaml | grep gend-cli

# 실행 (정비창 권장 — Keycloak 재시작 없음, sync Job 만 실행)
make keycloak-realm-sync KCTX=aks-genos-prod

# 검증
kubectl --context aks-genos-prod -n gend logs job/keycloak-realm-sync | grep -E "CREATE|UPDATE.*gend-cli"

⚠️ realm-sync-job 자체는 무중단 (Keycloak 데이터만 변경) 이지만, 잘못된 realm 정의가 들어가면 모든 사람 사용자가 즉시 로그인 차단된다. 정비창 + 즉시 roll-back 절차 (이전 realm-export.json revert + 재실행) 를 사전 합의한 상태에서 진행한다.

3. 검증

3.1 Discovery endpoint

curl -s https://gend.genon.ai/auth/realms/gend/.well-known/openid-configuration \
| jq '{issuer, device_authorization_endpoint, token_endpoint}'

AKS prod 검증 결과 (Epic #1309 정비창, 2026-05-28):

{
"issuer": "https://gend.genon.ai/auth/realms/gend",
"device_authorization_endpoint": "https://gend.genon.ai/auth/realms/gend/protocol/openid-connect/auth/device",
"token_endpoint": "https://gend.genon.ai/auth/realms/gend/protocol/openid-connect/token"
}

device_authorization_endpointnull 이 나오면 oauth2.device.authorization.grant.enabled 가 누락된 것.

3.2 Device endpoint 호출

Keycloak public client (gend-cli) 는 oauth2.pkce.code.challenge.method=S256 강제이므로 device endpoint 도 PKCE 동반 필요:

VERIFIER=$(openssl rand -base64 32 | tr -d '=' | tr '+/' '-_')
CHALLENGE=$(echo -n "$VERIFIER" | openssl dgst -sha256 -binary | base64 | tr -d '=' | tr '+/' '-_')

curl -s -X POST https://gend.genon.ai/auth/realms/gend/protocol/openid-connect/auth/device \
-d 'client_id=gend-cli' \
-d 'scope=openid profile email' \
-d "code_challenge=$CHALLENGE" \
-d 'code_challenge_method=S256' \
| jq '{verification_uri, user_code, expires_in, interval}'

AKS prod 검증 결과 (정비창):

{
"verification_uri": "https://gend.genon.ai/auth/realms/gend/device",
"user_code": "OPFU-HPMA",
"expires_in": 600,
"interval": 5
}

PKCE 미동반 시 {"error":"invalid_request","error_description":"Missing parameter: code_challenge_method"}. 관련 follow-up: #1361 — M4 initiate_device_flow 가 PKCE 미전송 → 본 검증에서 발견됨, CLI fix 필요.

3.3 회귀 가드 (CI 자동)

scripts/lint_gend_cli_keycloak_client.pyrealm-export.json 의 핵심 invariant (publicClient, PKCE S256, Device Flow enabled, redirectUris loopback, audience mapper) 를 검증한다. 위반 시 PR CI 가 차단한다.

수동 실행:

python3 scripts/lint_gend_cli_keycloak_client.py
python3 -m pytest scripts/test_lint_gend_cli_keycloak_client.py -v

3.4 실제 로그인 라운드트립

gend auth login # 브라우저 자동 진입
gend auth status # 세션 요약
gend catalog catalogs # API 호출 (자동 갱신 검증)
gend auth logout # 서버 + 로컬 cleanup

3.5 realm-sync Job 로그 — AKS prod 정비창 결과

make keycloak-realm-sync KCTX=aks-genos-prod 실행 후 Job 로그에서 CREATE client gend-cli + 2 mappers 확인:

$ kubectl --context aks-genos-prod -n gend logs job/keycloak-realm-sync \
| grep -E "CREATE|UPDATE.*gend-cli"
UPDATE client gend-ui (...)
...
CREATE client gend-cli
CREATE mapper gend-cli/gend-api-audience
CREATE mapper gend-cli/group-membership
UPDATE client trino (...)
...

Epic #1309 정비창 실행 (2026-05-28) 기록. 기존 client 들은 모두 UPDATE (secret 보존), gend-cli 만 신규 CREATE.

3.6 UI 모달 격하 스크린샷

ApiTokenDialog 격하 (AKS prod, 2026-05-29 캡처)

AKS prod 에서 실측. Epic #1309 M6 의 격하 시그널이 모두 노출됨:

  • 타이틀API 토큰 (데모/디버깅 전용)
  • 설명 — 일상 CLI 는 gend auth login (Loopback / Device Flow) 권장, CI/자동화는 /admin/service-clients (M2M)
  • amber notice — "즉석 curl / 토큰 클레임 디버깅 용도로만 권장" + gend auth login (사람 SSO) / /admin/service-clients (M2M) 두 alternative
  • 토큰 (마스킹) + 만료 시각 + CLI 사용 예시 (export GEND_API_URL=...)

캡처 자동화 (운영자 재현): ui/.local/capture-pr1352.mjs (본 worktree 의 gitignored 경로) 가 §3.7 의 M2M token 을 oidc.user:... localStorage 형식으로 주입한 뒤 Playwright 로 사용자 메뉴 → "API 토큰" click + dialog 크롭 캡처. 운영자 본인 SSO 비밀번호 없이 K8s Secret 만으로 진행 가능.

3.7 M2M token 자동 발급 (운영자 진단용)

ApiTokenDialog 스크린샷 / MCP 회귀 진단 등 audience=gend-api 토큰이 필요한 경우 gend-realm-sync (master service account) → gend-service (gend realm M2M) 체인으로 자동 발급한다:

SECRET=$(kubectl --context aks-genos-prod -n gend get secret gend-realm-sync \
-o jsonpath='{.data.client-secret}' | base64 -d)

# 1) master realm service account → admin-scope token
MASTER_TOK=$(curl -s -X POST \
https://gend.genon.ai/auth/realms/master/protocol/openid-connect/token \
-d 'grant_type=client_credentials' \
-d 'client_id=gend-realm-sync' \
-d "client_secret=$SECRET" | jq -r .access_token)

# 2) admin API 로 gend-service client secret 조회
SVC_ID=$(curl -s -H "Authorization: Bearer $MASTER_TOK" \
"https://gend.genon.ai/auth/admin/realms/gend/clients?clientId=gend-service" \
| jq -r '.[0].id')
SVC_SECRET=$(curl -s -H "Authorization: Bearer $MASTER_TOK" \
"https://gend.genon.ai/auth/admin/realms/gend/clients/$SVC_ID/client-secret" \
| jq -r .value)

# 3) gend realm client_credentials grant → audience=gend-api token
M2M_TOK=$(curl -s -X POST \
https://gend.genon.ai/auth/realms/gend/protocol/openid-connect/token \
-d 'grant_type=client_credentials' \
-d 'client_id=gend-service' \
-d "client_secret=$SVC_SECRET" | jq -r .access_token)

발급된 token claim 예 (Epic #1309 정비창 검증):

{
"aud": ["gend-api", "account"],
"azp": "gend-service",
"iss": "https://gend.genon.ai/auth/realms/gend",
"scope": "email profile"
}

3.8 MCP 회귀 검증 (사람 SSO 흐름 무영향 확인)

Epic #1309 의 realm 변경이 MCP 핸들러 (POST /mcp) 회귀를 일으키지 않는지 검증. MCP endpoint 는 Ingress 가 /api 만 라우팅하므로 직접 port-forward.

kubectl --context aks-genos-prod -n gend port-forward svc/gend-api 18000:8000 &
M2M_TOK=... # §3.7 참조

curl -s -X POST http://localhost:18000/mcp \
-H "Authorization: Bearer $M2M_TOK" \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","method":"tools/list","id":1}' \
| jq '.result.tools | length'

Epic #1309 정비창 검증 결과: 27 tools 노출 (list_catalogs / list_schemas / execute_query / list_intel_sources 등). audience=gend-api 토큰만 있으면 azp 가 gend-service 든 gend-cli 든 동일 핸들러 통과 — 사람 SSO 흐름 (azp=gend-cli) 에도 회귀 없음.

4. Roll-back

문제 발생 시 이전 realm-export.json 으로 revert + 재실행:

git revert <bad-commit>
git push origin main
# main 머지 후
make keycloak-realm-sync KCTX=aks-genos-prod

realm-sync-job 이 entity 별 idempotent 라 revert 도 안전. 단, 삭제는 명시 처리 필요gend-cli client 를 완전히 제거하려면 admin console 또는 kcadm delete clients/<id> 를 수동 호출.

5. 알려진 함정

함정대응
redirectUrishttp://127.0.0.1:* wildcard 가 Keycloak 17 미만에서 거부infra/keycloak/base/deployment.yaml 의 이미지 태그 확인 (현재 26.2)
AKS prod 의 issuer URL 이 CLI authority 와 다름 (Mixed Content 등)discovery.pymetadata override 지원, feedback_keycloak_oidc 메모 참조
gend auth login 실패 후 stale 키체인 entry 잔존gend auth logout 또는 keyring del gend-cli default 로 정리
Loopback 임의 포트가 사내 방화벽에 막힘--device 명시 또는 GEND_HEADLESS=1 설정
refresh_token rotation 시 멀티 터미널 raceTokenStore 의 fcntl 파일 잠금 — ~/.gend/token.lock

6. 관련

  • Epic #1309 — Human SSO CLI flow
  • #1310 M1 — Keycloak client
  • #1311 M2 — TokenStore + keyring
  • #1312 M3 — Loopback flow
  • #1313 M4 — Device flow
  • #1314 M5 — Auto-refresh middleware
  • #1315 M6 — Docs + UI demotion