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 groups → auth-service не сможет определить роль пользователя.