Files
2026-06-25 16:11:18 +09:00

11 KiB

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가 먼저 배포되어 있어야 한다.
  • ⚠️ Keycloak이 사내 CA 서명이면 root-ca-cert Secret이 먼저 있어야 한다. custom-values가 이를 /ca에 마운트하고 SSL_CERT_FILE=/ca/ca.crt로 신뢰시킨다. 없으면 vmauth가 OIDC discovery를 TLS 검증 못 해(x509: certificate signed by unknown authority) OIDC verifier 초기화 실패 → 모든 JWT 401. (퍼블릭 CA Keycloak이면 불필요 — 해당 블록 제거 가능)
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 <JWT>로 제시하고, vmauth가 Discovery 엔드포인트에서 공개키를 자동 fetch·rotate하여 서명을 검증한다.

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 미설정 — 미인증 요청은 401 (account 0 무인증 접근 차단)

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 예시:

{ "exp": 1771953418, "vm_access": { "metrics_account_id": 1, "logs_account_id": 1 } }

2.3 unauthorized_user — 사용 안 함

unauthorized_user를 두면 토큰 없음/무효 토큰 요청이 해당 accountID로 무인증 접근되어 격리가 약해진다(E2E에서 acct0 200 확인). 내부 서비스(vmalert)는 vmselect multitenant에 직결하므로 vmauth 폴백이 불필요 → 설정하지 않는다. 모든 vmauth 접근은 유효 JWT를 요구하며, 미인증은 401.

3. Keycloak 설정 (paasup realm)

배포 환경에는 항상 paasup realm이 존재한다. realm을 새로 만들지 않고 아래 리소스를 paasup realm에 추가한다. issuer는 https://keycloak.example.org/realms/paasup.

아래는 Keycloak Admin REST API(curl) 기준 명령이다. 별도 스크립트 파일은 제공하지 않으며, 운영자가 이 절차를 직접 실행/커스터마이징한다.

# 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에 공유한다.

# 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 접근
# 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가 필요 없다.

# 예: 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} 치환만 가능하다.

# 설정 변경 후 (예: 새 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. 배포 검증

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=<client>" -d "client_secret=<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(내부) ──직결──▶ vmselect /select/multitenant/prometheus   (vmauth 미경유)
미인증/무효토큰 ──▶ vmauth ──▶ 401 (unauthorized_user 없음)

쓰기 경로는 별도: vmagent ──remote_write──▶ vminsert:8480 (vmauth 미경유)