# vmauth 배포 (Keycloak JWT/OIDC 멀티테넌시) vmauth는 VictoriaMetrics 에코시스템의 인증·라우팅 프록시다. vmselect(메트릭)·vlselect(로그) 앞단에 위치해 클라이언트 요청을 단일 엔드포인트(`:8427`)로 받고, **Keycloak이 발급한 JWT를 검증한 뒤 테넌트(accountID)별로 라우팅**한다. ## 1. 배포 방법 ### 배포 시 주의사항 - vmauth **v1.138.0+** 부터 `jwt.oidc.issuer`로 OIDC Discovery 기반 JWT 검증을 지원한다 (Enterprise 아님). 본 카탈로그는 v1.144.0 기준이다. - 쓰기 경로(remote_write)는 vmauth를 경유하지 않는다. vmagent는 vminsert에 직접 쓴다. - vmcluster, (로그 사용 시) vlogs가 먼저 배포되어 있어야 한다. ```sh helm upgrade vmauth ./ -f custom-values.yaml --install -n monitoring ``` ## 2. custom-values.yaml 설정 설명 ### 2.1 인증 모델 — 전면 JWT/OIDC `config.users` 항목에 정적 password 대신 `jwt.oidc.issuer`를 선언한다. 클라이언트는 Keycloak 액세스 토큰을 `Authorization: Bearer `로 제시하고, vmauth가 Discovery 엔드포인트에서 공개키를 자동 fetch·rotate하여 서명을 검증한다. ```yaml config: users: - jwt: oidc: issuer: "https://keycloak.example.org/realms/paasup" url_map: # 메트릭: accountID를 URL 경로로 (VictoriaMetrics = 경로 기반) - src_paths: ["/api/v1/.*"] url_prefix: "http://...vmselect:8481/select/{{.MetricsAccountID}}/prometheus" # 로그: accountID를 HTTP 헤더로 (VictoriaLogs = 헤더 기반) - src_paths: ["/select/logsql/.*"] url_prefix: "http://...vlselect:9471" headers: - "AccountID: {{.LogsAccountID}}" - "ProjectID: 0" unauthorized_user: url_prefix: "http://...vmselect:8481/select/0/prometheus" ``` > **vm vs vl 격리 메커니즘이 다르다**: VictoriaMetrics는 accountID를 **URL 경로**(`/select/{N}/`)로, VictoriaLogs는 **HTTP 헤더**(`AccountID`/`ProjectID`)로 받는다. 동일한 토큰의 `vm_access`가 두 차원(`metrics_account_id`/`logs_account_id`)을 함께 담고, vmauth가 `src_paths`별로 경로 치환 vs 헤더 주입을 다르게 적용한다. 즉 **테넌트별 계정을 나눌 필요 없이 단일 vmauth 항목**으로 둘 다 격리된다. ### 2.2 vm_access 클레임 → accountID 동적 라우팅 토큰의 `vm_access` 클레임이 테넌트를 결정한다. vmauth는 `url_prefix`의 플레이스홀더로 이를 참조한다. | vm_access 필드 | 플레이스홀더 | 용도 | |----------------|-------------|------| | `metrics_account_id` | `{{.MetricsAccountID}}` | 메트릭 테넌트(vmselect `/select/{N}/`) | | `metrics_project_id` | `{{.MetricsProjectID}}` | 메트릭 프로젝트 | | (account+project) | `{{.MetricsTenant}}` | `accountID:projectID` 결합 | | `logs_account_id` | `{{.LogsAccountID}}` | 로그 테넌트 | JWT payload 예시: ```json { "exp": 1771953418, "vm_access": { "metrics_account_id": 1, "logs_account_id": 1 } } ``` ### 2.3 unauthorized_user 토큰 없이 들어오는 내부 서비스(vmalert 등)는 `unauthorized_user` 정책으로 accountID 0에 폴백된다. ## 3. Keycloak 설정 (paasup realm) > 배포 환경에는 **항상 `paasup` realm이 존재**한다. **realm을 새로 만들지 않고** 아래 리소스를 `paasup` realm에 추가한다. issuer는 `https://keycloak.example.org/realms/paasup`. > 아래는 **Keycloak Admin REST API(curl)** 기준 명령이다. **별도 스크립트 파일은 제공하지 않으며, 운영자가 이 절차를 직접 실행/커스터마이징한다.** ```sh # 0) 관리자 토큰 발급 + 공통 변수 KC=https://keycloak.example.org REALM=paasup TOKEN=$(curl -s -X POST "$KC/realms/master/protocol/openid-connect/token" \ -d grant_type=password -d client_id=admin-cli \ -d username=admin -d password="$KC_ADMIN_PW" | jq -r .access_token) H="Authorization: Bearer $TOKEN" JSON="Content-Type: application/json" ``` ### 3.1 Client Scope `vm-access` + Protocol Mapper (1회, 배포 시점) `vm_access` 클레임을 주입하는 매퍼를 Client Scope로 묶어 여러 Client에 공유한다. ```sh # Client Scope 생성 curl -s -X POST "$KC/admin/realms/$REALM/client-scopes" -H "$H" -H "$JSON" \ -d '{"name":"vm-access","protocol":"openid-connect"}' SID=$(curl -s "$KC/admin/realms/$REALM/client-scopes" -H "$H" \ | jq -r '.[]|select(.name=="vm-access").id') # metrics_account_id 매퍼 (claim.name에 점(.) → 중첩 JSON vm_access.metrics_account_id) curl -s -X POST "$KC/admin/realms/$REALM/client-scopes/$SID/protocol-mappers/models" -H "$H" -H "$JSON" -d '{ "name":"vm-metrics-account","protocol":"openid-connect", "protocolMapper":"oidc-usermodel-attribute-mapper", "config":{"user.attribute":"vm_metrics_account_id","claim.name":"vm_access.metrics_account_id", "jsonType.label":"int","access.token.claim":"true","id.token.claim":"false"}}' # logs_account_id 매퍼 curl -s -X POST "$KC/admin/realms/$REALM/client-scopes/$SID/protocol-mappers/models" -H "$H" -H "$JSON" -d '{ "name":"vm-logs-account","protocol":"openid-connect", "protocolMapper":"oidc-usermodel-attribute-mapper", "config":{"user.attribute":"vm_logs_account_id","claim.name":"vm_access.logs_account_id", "jsonType.label":"int","access.token.claim":"true","id.token.claim":"false"}}' ``` 결과 토큰: `{"vm_access":{"metrics_account_id":1,"logs_account_id":1}}` ### 3.2 Clients (1회, 배포 시점) | Client | 유형 | 용도 | |--------|------|------| | `perses` | confidential (Auth Code + PKCE) | Perses UI 로그인 (vmauth 쿼리 아님) | | `perses-vmauth` | confidential (Service Accounts / client_credentials) | Perses datasource → vmauth 쿼리 토큰 | | `vmauth-client` (선택) | public/confidential | curl·Grafana 등 직접 API 접근 | ```sh # Perses 로그인 Client curl -s -X POST "$KC/admin/realms/$REALM/clients" -H "$H" -H "$JSON" -d '{ "clientId":"perses","enabled":true,"publicClient":false,"standardFlowEnabled":true, "redirectUris":["https://perses.example.org/api/auth/providers/oidc/keycloak/callback"]}' # Perses datasource service-account Client curl -s -X POST "$KC/admin/realms/$REALM/clients" -H "$H" -H "$JSON" -d '{ "clientId":"perses-vmauth","enabled":true,"publicClient":false, "standardFlowEnabled":false,"serviceAccountsEnabled":true}' # vm-access Client Scope를 default로 연결 CID=$(curl -s "$KC/admin/realms/$REALM/clients?clientId=perses-vmauth" -H "$H" | jq -r '.[0].id') curl -s -X PUT "$KC/admin/realms/$REALM/clients/$CID/default-client-scopes/$SID" -H "$H" # perses-vmauth 서비스계정에 플랫폼 조회용 account attribute 설정 SAU=$(curl -s "$KC/admin/realms/$REALM/clients/$CID/service-account-user" -H "$H" | jq -r .id) curl -s -X PUT "$KC/admin/realms/$REALM/users/$SAU" -H "$H" -H "$JSON" -d '{ "attributes":{"vm_metrics_account_id":["0"],"vm_logs_account_id":["0"]}}' ``` > client_secret(`perses-vmauth`)은 perses `files/perses-provisioning.yaml`의 OAuth secret과 일치시켜야 한다 → **perses 차트 설치 전에 생성**한다. (secret 조회: `GET $KC/admin/realms/$REALM/clients/$CID/client-secret`) ### 3.3 테넌트 온보딩 (테넌트마다, 재배포 불필요) 새 테넌트 = `paasup` realm에 **Group 생성 + account 번호 결정 + 멤버 attribute 설정**. vmauth values 수정이나 `helm upgrade`가 필요 없다. ```sh # 예: demo01 = accountID 1 curl -s -X POST "$KC/admin/realms/$REALM/groups" -H "$H" -H "$JSON" -d '{"name":"tenant-demo01"}' # 사용자에 account attribute 부여 (그룹 가입과 별개로 토큰에는 user attribute가 실림) UID=$(curl -s "$KC/admin/realms/$REALM/users?username=alice" -H "$H" | jq -r '.[0].id') # 기존 표현을 GET 후 attributes만 병합해 PUT (다른 필드 보존) curl -s "$KC/admin/realms/$REALM/users/$UID" -H "$H" \ | jq '.attributes = (.attributes // {}) + {"vm_metrics_account_id":["1"],"vm_logs_account_id":["1"]}' \ | curl -s -X PUT "$KC/admin/realms/$REALM/users/$UID" -H "$H" -H "$JSON" -d @- # 그룹 가입 GID=$(curl -s "$KC/admin/realms/$REALM/groups?search=tenant-demo01" -H "$H" | jq -r '.[0].id') curl -s -X PUT "$KC/admin/realms/$REALM/users/$UID/groups/$GID" -H "$H" ``` > ⚠️ Keycloak에는 그룹 attribute→클레임 빌트인 매퍼가 없다. account 번호는 **사용자 attribute**에 들어가야 토큰에 실린다. 그룹은 관리 단위로 쓰고, 그룹 멤버십 변경 시 멤버 attribute를 채우는 운영 절차(또는 자동화)를 둔다. > > Perses 쪽 권한(Project/RoleBinding)은 perses `CUSTOM-README.md` 3장 참조(OIDC groups 미동기화 → UUID subject 수동 관리). > **vm/vl 비대칭**: 메트릭은 `/select/multitenant/`로 전체 집계 조회가 되지만, VictoriaLogs는 "모든 테넌트 합산" 엔드포인트가 없어 `(AccountID, ProjectID)` 단위로만 조회된다. 플랫폼 통합 로그 뷰는 특정 account(예: 0)로 한정된다. ### 3.4 설정 변경 시 hot reload vmauth config(`-auth.config`)는 정적 파일이지만 hot reload를 지원한다. 원격 URL/시크릿 로딩은 미지원이며 `%{ENV_VAR}` 치환만 가능하다. ```sh # 설정 변경 후 (예: 새 url_map 추가) kubectl exec -n monitoring deploy/vmauth-victoria-metrics-auth -- \ wget -qO- --post-data='' http://localhost:8427/-/reload # 또는 -configCheckInterval 플래그로 주기적 자동 재로딩 ``` ## 4. basic-auth fallback (선택) 토큰을 사용할 수 없는 CI/레거시 클라이언트에 한해 정적 basic-auth 항목을 추가한다. 평문 금지 — `%{ENV_VAR}` 치환을 사용한다(`custom-values.yaml` 주석 참조). ## 5. 배포 검증 ```sh kubectl port-forward -n monitoring svc/vmauth-victoria-metrics-auth 8427:8427 # Keycloak 토큰 발급 (client_credentials 예시) TOKEN=$(curl -s -d "grant_type=client_credentials" \ -d "client_id=" -d "client_secret=" \ "https://keycloak.example.org/realms/paasup/protocol/openid-connect/token" | jq -r .access_token) # JWT를 제시해 PromQL 조회 (vm_access의 accountID로 라우팅됨) curl -s -H "Authorization: Bearer $TOKEN" \ 'http://localhost:8427/api/v1/query?query=count(up)' | jq '.status' # → "success" ``` ## 6. 아키텍처 ``` 클라이언트 ──Bearer JWT──▶ vmauth:8427 ──(vm_access.accountID)──▶ vmselect:8481 /select/{N}/prometheus └──────────────────────▶ vlselect:9471 (로그) vmalert(내부) ──(토큰 없음)──▶ vmauth ──unauthorized_user──▶ vmselect /select/0/prometheus ``` > 쓰기 경로는 별도: vmagent ──remote_write──▶ vminsert:8480 (vmauth 미경유)