Перейти к содержанию

Keycloak

Keycloak — SSO-сервер аутентификации. Устанавливается только при auth.enabled: true. Создаёт realm appsec-genai, OIDC-клиенты и bootstrap-пользователя.

Назначение

Обеспечивает единую точку аутентификации для веб-интерфейса (OIDC через Envoy Gateway) и межсервисного взаимодействия (auth-service использует service account token exchange). Realm appsec-genai содержит 4 группы пользователей, 2 OIDC-клиента и protocol mapper для передачи групп в JWT.

Зависимости

Входящие

Сервис Как использует
genai-gateway OIDC provider для SecurityPolicy (клиент envoy-gateway)
auth-service token validation, admin API (клиент auth-service)
Браузер web console: https://keycloak.<domain>

Исходящие

Сервис Назначение
postgres backend БД (init-контейнер создаёт схему)

Values

Параметр По умолчанию Обязателен Описание
persistence.enabled false нет Включить PVC
persistence.size "1Gi" нет Размер PVC
persistence.storageClass "" нет StorageClass
global.domain да Базовый домен (KC_HOSTNAME)
global.publicScheme "http" нет https для prod
global.instance "" нет Суффикс для мультиинсталляций
global.deps.postgres.host postgres нет Хост PostgreSQL
global.deps.postgres.port 5432 нет Порт PostgreSQL
global.deps.postgres.username genai_admin нет Пользователь PostgreSQL
global.deps.postgres.database genai_db нет База данных
global.deps.postgres.existingSecret postgres-auth да Secret с POSTGRES_PASSWORD
global.image.tag да Версия образа
global.imagePullSecrets[0].name да imagePullSecret

Пример values.yaml

# values-keycloak.yaml
persistence:
  enabled: true
  size: "1Gi"
  storageClass: ""

global:
  domain: "app.example.com"
  publicScheme: "https"
  # instance: "rc"          # раскомментировать для мультиинсталляции

  image:
    registry: registry.appsec.global
    repositoryPrefix: appsecgenai-release
    tag: "<VERSION>"
  imagePullSecrets:
    - name: harbor-cr

  deps:
    postgres:
      host: postgres
      port: 5432
      username: genai_admin
      database: genai_db
      existingSecret: postgres-auth   # Secret с POSTGRES_PASSWORD

Установка

helm upgrade --install keycloak \
  oci://registry.appsec.global/appsecgenai-release/charts/keycloak \
  --version <VERSION> -n genai --create-namespace \
  --wait \
  -f values-keycloak.yaml

После установки чарт создаёт Secret keycloak-auth со следующими ключами:

Ключ Назначение
KEYCLOAK_ADMIN_PASSWORD Пароль admin-пользователя консоли
KEYCLOAK_BOOTSTRAP_USER_PASSWORD Пароль bootstrap-пользователя genai-admin
AUTH_SERVICE_CLIENT_SECRET Client secret для auth-service
ENVOY_GATEWAY_CLIENT_SECRET Client secret для genai-gateway OIDC
# Получить admin password
kubectl -n genai get secret keycloak-auth \
  -o jsonpath='{.data.KEYCLOAK_ADMIN_PASSWORD}' | base64 -d

Внешний Keycloak

Если Keycloak уже развёрнут отдельно — установите keycloak.enabled: false и укажите координаты в global.deps.keycloak.

Wizard (values.yaml)

keycloak:
  enabled: false          # не ставить in-cluster Keycloak

global:
  deps:
    keycloak:
      host: keycloak.example.com
      port: 443
      scheme: https        # https если Keycloak за TLS (default: http)
      # bootstrap: true    # создать realm appsec-genai через Admin REST API

bootstrap: идемпотентность

Realm и клиенты создаются через Admin REST API — повторный запуск возвращает 409 Conflict (пропуск). Оставить bootstrap: true на каждый update — безопасно. При встроенном Keycloak (keycloak.enabled: true) bootstrap не нужен — realm создаётся при первом старте контейнера.

bootstrap: true для внешнего Keycloak

При keycloak.bootstrap: true Wizard создаёт realm через Admin API. Требует два Secret'а в namespace:

# Admin credentials для создания realm
kubectl create secret generic keycloak-admin -n genai \
  --from-literal=KEYCLOAK_ADMIN="admin" \
  --from-literal=KEYCLOAK_ADMIN_PASSWORD="<admin-password>"

# Client secrets для Envoy Gateway и auth-service
kubectl create secret generic keycloak-auth -n genai \
  --from-literal=ENVOY_GATEWAY_CLIENT_SECRET="<uuid>" \
  --from-literal=AUTH_SERVICE_CLIENT_SECRET="<uuid>"

Bootstrap realm (при ручной установке Keycloak)

Если Keycloak устанавливается не через наш чарт — создайте realm вручную. Самый простой способ — импортировать JSON через Admin API.

Шаг 1: Сгенерировать client secrets

# Сгенерировать два UUID для client secrets
ENVOY_SECRET=$(uuidgen | tr '[:upper:]' '[:lower:]')
AUTH_SECRET=$(uuidgen | tr '[:upper:]' '[:lower:]')

echo "ENVOY_GATEWAY_CLIENT_SECRET=$ENVOY_SECRET"
echo "AUTH_SERVICE_CLIENT_SECRET=$AUTH_SECRET"

Сохраните эти значения — они понадобятся и для Secret'а keycloak-auth, и для realm JSON.

Шаг 2: Создать Secret keycloak-auth

kubectl create secret generic keycloak-auth -n genai \
  --from-literal=KEYCLOAK_ADMIN_PASSWORD="<keycloak-admin-password>" \
  --from-literal=ENVOY_GATEWAY_CLIENT_SECRET="$ENVOY_SECRET" \
  --from-literal=AUTH_SERVICE_CLIENT_SECRET="$AUTH_SECRET"

Шаг 3: Импортировать realm

Подставьте <DOMAIN>, <ENVOY_SECRET> и <AUTH_SECRET> в шаблон и импортируйте:

DOMAIN="app.example.com"
SCHEME="https"

cat > /tmp/realm-appsec-genai.json << EOF
{
  "realm": "appsec-genai",
  "enabled": true,
  "displayName": "AppSec GenAI Platform",
  "registrationAllowed": false,
  "loginWithEmailAllowed": true,
  "duplicateEmailsAllowed": false,
  "sslRequired": "external",
  "accessTokenLifespan": 300,
  "ssoSessionIdleTimeout": 1800,
  "ssoSessionMaxLifespan": 36000,
  "defaultGroups": ["/asg-viewer"],
  "groups": [
    {"name": "asg-viewer",     "path": "/asg-viewer"},
    {"name": "asg-user",       "path": "/asg-user"},
    {"name": "asg-power-user", "path": "/asg-power-user"},
    {"name": "asg-admin",      "path": "/asg-admin"}
  ],
  "clientScopes": [
    {
      "name": "groups",
      "protocol": "openid-connect",
      "attributes": {"include.in.token.scope": "true"},
      "protocolMappers": [
        {
          "name": "groups",
          "protocol": "openid-connect",
          "protocolMapper": "oidc-group-membership-mapper",
          "consentRequired": false,
          "config": {
            "full.path": "false",
            "id.token.claim": "true",
            "access.token.claim": "true",
            "claim.name": "groups",
            "userinfo.token.claim": "true"
          }
        }
      ]
    }
  ],
  "clients": [
    {
      "clientId": "envoy-gateway",
      "enabled": true,
      "protocol": "openid-connect",
      "publicClient": false,
      "clientAuthenticatorType": "client-secret",
      "secret": "$ENVOY_SECRET",
      "standardFlowEnabled": true,
      "directAccessGrantsEnabled": false,
      "serviceAccountsEnabled": false,
      "rootUrl": "${SCHEME}://${DOMAIN}",
      "baseUrl": "${SCHEME}://${DOMAIN}/",
      "redirectUris": ["${SCHEME}://${DOMAIN}/*"],
      "webOrigins": ["${SCHEME}://${DOMAIN}"],
      "attributes": {
        "post.logout.redirect.uris": "${SCHEME}://${DOMAIN}/*##${SCHEME}://${DOMAIN}/##${SCHEME}://${DOMAIN}"
      },
      "defaultClientScopes": ["openid", "email", "profile", "groups"]
    },
    {
      "clientId": "auth-service",
      "enabled": true,
      "protocol": "openid-connect",
      "publicClient": false,
      "clientAuthenticatorType": "client-secret",
      "secret": "$AUTH_SECRET",
      "standardFlowEnabled": true,
      "directAccessGrantsEnabled": true,
      "serviceAccountsEnabled": true,
      "redirectUris": [],
      "webOrigins": [],
      "defaultClientScopes": ["openid", "email", "profile", "groups"]
    }
  ]
}
EOF

# Получить admin token
ADMIN_TOKEN=$(curl -s -X POST \
  "https://keycloak.${DOMAIN}/realms/master/protocol/openid-connect/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=password&client_id=admin-cli&username=admin&password=<admin-password>" \
  | python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])")

# Импортировать realm
curl -s -X POST \
  "https://keycloak.${DOMAIN}/admin/realms" \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d @/tmp/realm-appsec-genai.json \
  -w "\nHTTP %{http_code}\n"

Ожидаемый ответ: HTTP 201

Структура realm

Группы (все новые пользователи попадают в asg-viewer по умолчанию):

Группа Роль в системе
asg-viewer Только просмотр результатов
asg-user Запуск сканирований
asg-power-user Расширенные права
asg-admin Полный доступ

Клиенты:

Client ID Тип Назначение
envoy-gateway Confidential, Standard Flow OIDC для браузерной аутентификации через Envoy
auth-service Confidential, Service Account Service-to-service token validation

Protocol mapper groups — присутствует в scope groups, включён в оба клиента. Передаёт список групп пользователя в JWT claim groups (short path, без / префикса).

groups scope в defaultClientScopes

Оба клиента должны иметь groups в defaultClientScopes. Без этого JWT не будет содержать claim groupsauth-service не сможет определить роль пользователя.