M2M 서비스 계정 인증
quant-ai 같은 외부 서비스나 자동화 파이프라인이 사용자 JWT 없이 GenD API를 호출할 수 있도록, Keycloak client_credentials grant 기반 서비스 계정 토큰을 지원합니다.
개인이 노트북에서 gend-cli 를 쓰는 경우 — 본 문서가 아니라 CLI SSO 운영 + CLI 빠른 시작 (사람용 SSO) 을 참조하세요. 본 문서는 머신 / CI / 외부 서비스 전용입니다.
언제 쓰는가
| 상황 | 사용 토큰 |
|---|---|
| 사용자가 브라우저로 로그인 후 호출 | 사용자 JWT (Authorization Code + PKCE) |
| 개인이 노트북/CLI 에서 호출 | gend auth login 으로 SSO 토큰 (Epic #1309) |
| 외부 서비스/스크립트/스케줄러가 호출 | 서비스 계정 토큰 (client_credentials) ← 본 문서 |
아키텍처
운영자 사전 작업 — Realm 역할 부여
처음 gend-service 클라이언트를 배포한 직후, 서비스 계정 사용자 (service-account-gend-service)에 read-only 권한을 부여해야 라우터의 require_viewer / require_analyst 가드를 통과합니다.
# viewer (catalog/quality/governance 조회) — 최소 권한
kubectl --context aks-genos-prod exec -n gend deploy/keycloak -- \
/opt/keycloak/bin/kcadm.sh add-roles -r gend \
--uusername=service-account-gend-service --rolename=viewer
# analyst (query 실행 포함) — 필요 시
kubectl --context aks-genos-prod exec -n gend deploy/keycloak -- \
/opt/keycloak/bin/kcadm.sh add-roles -r gend \
--uusername=service-account-gend-service --rolename=analyst
해당 작업이 누락되면 gend-service 토큰은 인증은 통과하지만 (200) 모든 read-only 엔드포인트가 403 권한 부족 을 반환합니다. 코드 측 is_service_account 로직은 토큰 식별과 audit 표기 용도이며, 라우터 권한은 실제 realm role 로 결정됩니다.
권한 모델
라우터 권한 매트릭스:
| 라우터 | 필요 realm role | admin 사용자 |
|---|---|---|
GET /api/v1/catalog/* | viewer | ✅ |
POST /api/v1/query/execute | analyst | ✅ |
GET /api/v1/quality/* | viewer | ✅ |
GET /api/v1/governance/* | viewer | ✅ |
GET /api/v1/features/* | viewer | ✅ |
POST/PUT/DELETE (변경) | admin 전용 | ✅ |
/api/v1/admin/users/* | admin 전용 | ✅ |
서비스 계정 토큰은 admin 자동 승격이 적용되지 않습니다 — 변경 작업이 필요하면 별도 admin role 이 부여된 서비스 클라이언트를 발급하거나 사용자 JWT 를 사용하세요.
서비스 클라이언트 발급 — GenD UI (#654)
GenD admin 사용자는 UI 에서 외부 서비스용 클라이언트를 자체 발급/회전/관리할 수 있다 (Keycloak Admin Console 직접 접근 불필요).
진입 경로
사이드바 하단 ⚙ 관리 콘솔 → 테넌트 & 사용자 → 서비스 클라이언트 (/admin/service-clients)
새 클라이언트 발급
- "새 발급" 버튼 클릭
- 입력
- Client ID 접미사 —
gend-svc-prefix 가 자동으로 붙는다. 사용자는 suffix(예:quant-ai) 만 입력 → 최종gend-svc-quant-ai - 설명 — 호출 주체 식별용 (자유 텍스트)
- Role —
viewer(catalog/quality 조회) 또는analyst(query 실행 포함)
- Client ID 접미사 —
- 생성 직후 시크릿이 1회만 모달로 표시된다 — 즉시 외부 서비스에 안전 저장 (Vault / Secret Manager). 모달을 닫으면 다신 조회 불가.
시크릿 회전
액션 메뉴 → "Secret 회전" → 확인 → 신규 시크릿 1회 표시. 이전 시크릿은 즉시 무효화되며, 외부 서비스가 새 시크릿으로 갱신될 때까지 401 을 받는다.
비활성화 / 삭제
- 비활성화 — 토큰 발급 차단. 기존 토큰은 만료(
accessTokenLifespan, 30분) 시까지 유효. - 삭제 — 즉시 client 제거. 발급된 토큰은 전부 무효.
보안
- prefix
gend-svc-강제 — 시스템 clientgend-service/ 예약어 (system,admin,internal) 차단. - 시스템 client
gend-service는 본 UI 에서 노출/편집 불가. - 모든 발급/회전/삭제 액션은 audit 로그에
actor_type=user,target=<client_id>로 기록된다.
토큰 발급 (외부 서비스 측)
옵션 A — GenD UI 로 발급 (권장)
위 절차로 시크릿을 받은 뒤 3. GenD API 호출 로 진행.
옵션 B — kcadm.sh 로 직접 추출 (레거시 / UI 미배포 환경 전용)
아래 절차는 시스템 client gend-service 의 시크릿을 추출해 여러 서비스가 공유하는 #654 이전 패턴입니다. 자격증명이 유출되면 어느 서비스에서 샜는지 특정할 수 없고, 회수하면 공유 중인 모든 연동이 동시에 끊깁니다. 감사 로그에서도 호출 주체가 구분되지 않습니다.
신규 연동은 반드시 옵션 A 로 서비스별 클라이언트를 발급하세요. 본 절차는 UI 가 배포되지 않은 환경과 기존 공유 연동의 마이그레이션 참조용으로만 남겨둡니다.
gend-service 클라이언트의 시크릿은 Keycloak 자동 생성값입니다. 운영자가 한 번 추출하여 외부 서비스에 전달합니다.
SECRET=$(kubectl --context aks-genos-prod exec -n gend deploy/keycloak -- \
/opt/keycloak/bin/kcadm.sh get clients -r gend \
-q clientId=gend-service --fields id --format csv --noquotes \
| tail -1 \
| xargs -I{} /opt/keycloak/bin/kcadm.sh get clients/{}/client-secret -r gend --format csv --noquotes \
| tail -1)
echo "$SECRET"
운영 환경에서는 이 값을 SealedSecret (gend-service-client-secret) 으로 저장해 realm-sync-job이 단일 진실 공급원으로 강제 정렬하도록 권장합니다 — 단, 이는 선택 사항이며 미배포 시 Keycloak 자동 생성값이 그대로 유지됩니다.
2. 외부 서비스에서 토큰 발급
TOKEN=$(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="$SECRET" \
| jq -r .access_token)
토큰 수명은 realm 설정 accessTokenLifespan 을 따르며 — 1800초 (30분), realm-export.json:28 선언값과 라이브 realm 이 일치 — 만료 시 재발급합니다. refresh token은 발급되지 않습니다 (client_credentials 표준).
accessTokenLifespan 은 realm 설정이라 운영 중 변경될 수 있습니다 (실제로 2026-07-20 이전 라이브 값이 300 으로 드리프트해 있다가 선언값 1800 으로 정렬됨). 자동화 코드는 상수 대신 토큰 응답의 expires_in 을 사용하세요.
3. GenD API 호출
curl -H "Authorization: Bearer $TOKEN" \
https://gend.genon.ai/api/v1/catalog/catalogs
quant-ai 통합 예시 (Python)
import os
import time
import httpx
KC_URL = "https://gend.genon.ai/auth"
GEND_API = "https://gend.genon.ai"
CLIENT_ID = os.environ["GEND_CLIENT_ID"] # 예: gend-svc-quant-ai
CLIENT_SECRET = os.environ["GEND_CLIENT_SECRET"]
class GendClient:
def __init__(self):
self._token = None
self._token_expires = 0
def _ensure_token(self):
if self._token and time.time() < self._token_expires - 60:
return
resp = httpx.post(
f"{KC_URL}/realms/gend/protocol/openid-connect/token",
data={
"grant_type": "client_credentials",
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET,
},
)
resp.raise_for_status()
body = resp.json()
self._token = body["access_token"]
self._token_expires = time.time() + body["expires_in"]
def get_catalogs(self):
self._ensure_token()
resp = httpx.get(
f"{GEND_API}/api/v1/catalog/catalogs",
headers={"Authorization": f"Bearer {self._token}"},
)
resp.raise_for_status()
return resp.json()
def execute_query(self, sql: str):
self._ensure_token()
resp = httpx.post(
f"{GEND_API}/api/v1/query/execute",
headers={"Authorization": f"Bearer {self._token}"},
json={"sql": sql},
)
resp.raise_for_status()
return resp.json()
감사 로그
서비스 토큰 호출은 audit 로그에서 actor_type=service, user_id=<client_id> (예: gend-service) 로 식별됩니다. 사용자 호출 (actor_type=user) 과 분리하여 OpenSearch 필터링 가능합니다.
{
"event": "api_access",
"user_id": "gend-service",
"actor_type": "service",
"method": "GET",
"endpoint": "/api/v1/catalog/catalogs",
"status_code": 200,
"duration_ms": 12.4,
"client_ip": "10.0.4.21"
}
보안 고려사항
- 시크릿 회전: 서비스 시크릿은 외부 서비스 주입 환경 (env var, K8s Secret) 에서 관리. 노출 의심 시 GenD UI 의 Secret 회전 으로 즉시 무효화 후 외부 서비스 갱신 (Keycloak Admin Console 직접 접근 불필요). 시스템 client
gend-service만 예외적으로 Admin Console 의 Clients > gend-service > Credentials > Regenerate Secret 을 사용합니다. - 권한 최소화: 외부 서비스마다 개별 클라이언트를 발급하세요 (
gend-svc-quant-ai,gend-svc-mcp-gena등). 사용처별로 분리하면 감사 로그에서 호출 주체가 구분되고, 유출 시 해당 클라이언트만 회수하면 됩니다. 시스템 clientgend-service를 여러 서비스가 공유하는 방식은 #654 이전 패턴이며 신규 연동에는 사용하지 않습니다. - 변경 작업 차단: 서비스 토큰은 자동으로
admin권한을 받지 않으므로 POST/PUT/DELETE 라우터는 차단됩니다. 의도적으로 admin 권한이 필요한 자동화는 별도 클라이언트 + service-account-roles 매핑으로 분리하세요. - Audience 매퍼:
gend-service클라이언트에는gend-apiaudience 매퍼가 필수입니다 (infra/keycloak/base/realm-export.json참조). 미설정 시gend-api의 audience 검증에서 토큰이 거부됩니다.
트러블슈팅
| 증상 | 원인 / 해결 |
|---|---|
401 Invalid authentication token | aud 검증 실패 — gend-api-audience 매퍼 누락 또는 잘못된 client_id 로 발급 |
403 권한 부족 (조회 엔드포인트) | realm role 미부여 — 발급 시 role 을 지정하지 않았거나 서비스 계정에 viewer/analyst 가 없음. 위 운영자 사전 작업 참조. 조회 403 의 최빈 원인 |
403 권한 부족 (변경 엔드포인트) | 변경 작업 (POST/PUT/DELETE) 호출 — service 토큰은 admin 자동 승격 없음 |
| MCP 는 되는데 REST 만 403 | 정상 동작 — MCP 는 MCPGovernor, REST 는 realm role 가드로 판정 경로가 다름. REST 도 쓰려면 role 부여 필요 |
401 Token has expired | accessTokenLifespan (30분) 초과 — 재발급. 클라이언트는 expires_in 기준으로 선제 갱신 |
400 unauthorized_client | client_secret 불일치 — Keycloak 회전 후 외부 서비스 미동기화 |
관련 이슈
- #642 — 본 기능 도입