본문으로 건너뛰기

docs-qa 자동화 자격증명

~/.claude/commands/docs-qa-cycle.md skill 이 AKS prod 베타에서 41+ 페이지를 자동 점검하기 위해 사용하는 Keycloak 자격증명 관리 가이드.

배경 — cycle 18 #1405 차단

cycle 17 까지는 admin/admin 데모 자격이 동작해서 skill 의 OIDC 세션 주입 패턴이 hardcoded fallback (process.env.GEND_USER || 'admin') 으로 우회했다. cycle 18 진입 시 운영 측 W2 시리즈 머지 (#1371-#1379, #1394 Keycloak realm-export) 후 다음이 발생:

  • gend realm admin 사용자의 비밀번호가 변경됨 (또는 brute force 15분 잠금)
  • master realm admin-cli 클라이언트가 confidential 로 전환 (client_secret 필수)
  • KEYCLOAK_ADMIN_PASSWORD env 도 무효 (Keycloak 26+ 첫 부트스트랩 이후 환경변수 무시)

결과: docs-qa 사이클 18 의 Phase 1+1.7 통합 스윕 즉시 401 차단 → 향후 모든 사이클 차단.

복구 절차 (#1405 회복 산물)

Step A — prod 즉시 차단 해제 (DB 직접 작업)

운영자가 prod 자격을 잃거나 admin/admin 데모가 deprecated 된 경우 사용:

# 0) Keycloak Pod + PostgreSQL Pod + secret 확인 (이름은 환경마다 다르므로 동적 추출)
KC_POD=$(kubectl --context aks-genos-prod get pod -n gend -l app=keycloak -o jsonpath='{.items[0].metadata.name}')
PG_POD=$(kubectl --context aks-genos-prod get pod -n gend -l app.kubernetes.io/name=postgresql -o jsonpath='{.items[0].metadata.name}')
PG_PWD=$(kubectl --context aks-genos-prod get secret gend-pg-secret -n gend -o jsonpath='{.data.password}' | base64 -d)

# 1) argon2id hash 생성 (Keycloak 26+ default: argon2id, time_cost=5, memory=7168)
pip3 install argon2-cffi
TEMP_PWD="DocsQaBot$(date +%s)"
python3 <<PYEOF
import os, base64, json, uuid
from argon2.low_level import hash_secret_raw, Type
pwd = "$TEMP_PWD".encode()
salt = os.urandom(16)
raw = hash_secret_raw(secret=pwd, salt=salt, time_cost=5, memory_cost=7168, parallelism=1, hash_len=32, type=Type.ID)
print("USER_ID=" + str(uuid.uuid4()))
print("CRED_ID=" + str(uuid.uuid4()))
print("SECRET_DATA=" + json.dumps({"value": base64.b64encode(raw).decode(), "salt": base64.b64encode(salt).decode(), "additionalParameters": {}}))
print("CREDENTIAL_DATA=" + json.dumps({"hashIterations": 5, "algorithm": "argon2", "additionalParameters": {"hashLength": ["32"], "memory": ["7168"], "type": ["id"], "version": ["1.3"], "parallelism": ["1"]}}))
PYEOF

# 2) master realm 에 임시 admin user 추가 (위 USER_ID/CRED_ID/SECRET_DATA/CREDENTIAL_DATA 변수 사용)
kubectl --context aks-genos-prod exec -i -n gend $PG_POD -c postgresql -- bash -c "
PGPASSWORD='$PG_PWD' psql -U gend -d keycloak <<EOF
BEGIN;
INSERT INTO user_entity (id, realm_id, username, email, enabled, email_verified, created_timestamp)
SELECT '$USER_ID', id, 'docs-qa-bot', 'docs-qa-bot@gend.local', true, true, EXTRACT(EPOCH FROM NOW())*1000
FROM realm WHERE name='master';
INSERT INTO credential (id, user_id, type, secret_data, credential_data, created_date, priority)
VALUES ('$CRED_ID', '$USER_ID', 'password', '$SECRET_DATA', '$CREDENTIAL_DATA', EXTRACT(EPOCH FROM NOW())*1000, 10);
INSERT INTO user_role_mapping (user_id, role_id)
SELECT '$USER_ID', kr.id FROM keycloak_role kr JOIN realm r ON kr.realm_id=r.id
WHERE r.name='master' AND kr.name='admin';
COMMIT;
EOF
"

# 3) admin-cli secret 추출
ADMIN_CLI_SECRET=$(kubectl --context aks-genos-prod exec -i -n gend $PG_POD -c postgresql -- bash -c "
PGPASSWORD='$PG_PWD' psql -U gend -d keycloak -t -c \"SELECT c.secret FROM client c JOIN realm r ON c.realm_id=r.id WHERE r.name='master' AND c.client_id='admin-cli';\"
" | tr -d ' \n')

# 4) docs-qa-bot 으로 master 토큰 발급 → gend admin 비밀번호 reset + brute-force 해제
TOKEN=$(curl -sS https://gend.genon.ai/auth/realms/master/protocol/openid-connect/token \
-d "client_id=admin-cli&client_secret=${ADMIN_CLI_SECRET}&grant_type=password&username=docs-qa-bot&password=${TEMP_PWD}" \
| python3 -c "import sys, json; print(json.load(sys.stdin)['access_token'])")

ADMIN_ID=$(curl -sS -H "Authorization: Bearer $TOKEN" "https://gend.genon.ai/auth/admin/realms/gend/users?username=admin" \
| python3 -c "import sys, json; print(json.load(sys.stdin)[0]['id'])")

curl -sS -X DELETE -H "Authorization: Bearer $TOKEN" \
"https://gend.genon.ai/auth/admin/realms/gend/attack-detection/brute-force/users/${ADMIN_ID}"

curl -sS -X PUT -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"type":"password","value":"admin","temporary":false}' \
"https://gend.genon.ai/auth/admin/realms/gend/users/${ADMIN_ID}/reset-password"

# 5) 검증 — admin/admin 토큰 발급 성공해야 함
curl -sS https://gend.genon.ai/auth/realms/gend/protocol/openid-connect/token \
-d "client_id=gend-ui&grant_type=password&username=admin&password=admin&scope=openid" \
| python3 -c "import sys, json; d=json.load(sys.stdin); print('OK' if 'access_token' in d else 'FAIL', d)"

Step B — 향후 차단 방지 (service-account)

B1. realm-export.json 에 gend-docs-qa-bot client 추가

본 PR 의 infra/keycloak/base/realm-export.json 변경분이 이미 정의함. 새 client:

  • clientId: gend-docs-qa-bot
  • serviceAccountsEnabled: true
  • publicClient: false, directAccessGrantsEnabled: false
  • secret: PLACEHOLDER_SECRET_SET_VIA_KCADM ← 운영자가 prod 적용 시 변경

B2. prod 에 client 등록 + secret 발급

# (위 Step A 의 docs-qa-bot master admin 토큰 + admin-cli secret 활용)
TOKEN=... # Step A 의 docs-qa-bot 토큰

# B2a. client 생성
cat > /tmp/qa-bot-client.json <<EOF
{
"clientId": "gend-docs-qa-bot",
"name": "GenD docs-qa automation bot",
"enabled": true,
"publicClient": false,
"directAccessGrantsEnabled": false,
"serviceAccountsEnabled": true,
"clientAuthenticatorType": "client-secret"
}
EOF
curl -sS -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d @/tmp/qa-bot-client.json \
"https://gend.genon.ai/auth/admin/realms/gend/clients"

# B2b. service-account user 에게 read-only role 부여
QA_CLIENT_ID=$(curl -sS -H "Authorization: Bearer $TOKEN" \
"https://gend.genon.ai/auth/admin/realms/gend/clients?clientId=gend-docs-qa-bot" \
| python3 -c "import sys, json; print(json.load(sys.stdin)[0]['id'])")
QA_SA_USER=$(curl -sS -H "Authorization: Bearer $TOKEN" \
"https://gend.genon.ai/auth/admin/realms/gend/clients/${QA_CLIENT_ID}/service-account-user" \
| python3 -c "import sys, json; print(json.load(sys.stdin)['id'])")

# 적절한 read-only role 부여 (예: viewer, audit-reader 등 — 운영자가 결정)
# 또는 GenD 의 read-only role 매핑

# B2c. secret 추출
QA_SECRET=$(curl -sS -H "Authorization: Bearer $TOKEN" \
"https://gend.genon.ai/auth/admin/realms/gend/clients/${QA_CLIENT_ID}/client-secret" \
| python3 -c "import sys, json; print(json.load(sys.stdin)['value'])")
echo "Save this secret in ~/.gend/secrets/docs-qa-bot-secret.txt: $QA_SECRET"

B3. docs-qa skill 환경변수 주입

# 옵션 1: dotenv
mkdir -p ~/.gend/secrets && chmod 700 ~/.gend/secrets
cat > ~/.gend/secrets/docs-qa-creds.json <<EOF
{
"GEND_USER": "admin",
"GEND_PASS": "admin",
"GEND_DOCS_QA_BOT_SECRET": "<paste from B2c>"
}
EOF
chmod 600 ~/.gend/secrets/docs-qa-creds.json

# 옵션 2: direnv (.envrc — gitignore)
echo 'export $(jq -r "to_entries[] | \"\(.key)=\(.value)\"" ~/.gend/secrets/docs-qa-creds.json | xargs)' >> .envrc
direnv allow

B4. Step A 의 임시 docs-qa-bot user 정리

복구 작업 완료 후 master realm 의 임시 admin 제거:

TOKEN=...
TEMP_USER_ID=$(curl -sS -H "Authorization: Bearer $TOKEN" \
"https://gend.genon.ai/auth/admin/realms/master/users?username=docs-qa-bot" \
| python3 -c "import sys, json; print(json.load(sys.stdin)[0]['id'])")
curl -sS -X DELETE -H "Authorization: Bearer $TOKEN" \
"https://gend.genon.ai/auth/admin/realms/master/users/${TEMP_USER_ID}"

docs-qa skill 동작 — 자격 우선순위

~/.claude/commands/docs-qa-cycle.md Phase 0.4 의 OIDC 토큰 발급 순서:

  1. GEND_DOCS_QA_BOT_SECRET 존재 시client_credentials grant 시도 (Method A). UI 렌더 검증 불가, read-only API 호출만.
  2. GEND_USER + GEND_PASS 존재 시password grant (Method B). UI 렌더 + 인터랙션 가능.
  3. 둘 다 부재 시 → 즉시 throw (admin/admin 자동 fallback 제거). 운영자가 자격 변경 시점에 docs-qa 가 명확히 차단되도록 (silent fail 방지).

관련 자료

  • 이슈 #1405 — docs-qa: AKS prod admin/admin password grant 회귀
  • skill: ~/.claude/commands/docs-qa-cycle.md Phase 0.4
  • realm-export: infra/keycloak/base/realm-export.json (gend-docs-qa-bot client)
  • 사이클 18 산출물: docs-qa/cycle-18/